Add EuiCodeEditor to ES UI Shared. (#108318)

* Export EuiCodeEditor from es_ui_shared and consume it in Grok Debugger. Remove warning from EuiCodeEditor.
* Lazy-load code editor so it doesn't bloat the EsUiShared plugin bundle.
* Refactor mocks into a shared jest_mock.tsx file.
This commit is contained in:
CJ Cenizal 2021-08-13 16:49:55 -07:00 committed by GitHub
parent 5ef1f95711
commit bfea4a1c2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1200 additions and 94 deletions

View file

@ -336,7 +336,7 @@
"re-resizable": "^6.1.1",
"re2": "^1.15.4",
"react": "^16.12.0",
"react-ace": "^5.9.0",
"react-ace": "^7.0.5",
"react-beautiful-dnd": "^13.0.0",
"react-color": "^2.13.8",
"react-dom": "^16.12.0",

View file

@ -0,0 +1,627 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EuiCodeEditor behavior hint element should be disabled when the ui ace box gains focus 1`] = `
<button
class="euiCodeEditorKeyboardHint"
data-test-subj="codeEditorHint"
id="generated-id_codeEditor"
>
<p
class="euiText"
>
Press Enter to start editing.
</p>
<p
class="euiText"
>
When you're done, press Escape to stop editing.
</p>
</button>
`;
exports[`EuiCodeEditor behavior hint element should be enabled when the ui ace box loses focus 1`] = `
<button
class="euiCodeEditorKeyboardHint"
data-test-subj="codeEditorHint"
id="generated-id_codeEditor"
>
<p
class="euiText"
>
Press Enter to start editing.
</p>
<p
class="euiText"
>
When you're done, press Escape to stop editing.
</p>
</button>
`;
exports[`EuiCodeEditor behavior hint element should be tabable 1`] = `
<button
class="euiCodeEditorKeyboardHint"
data-test-subj="codeEditorHint"
id="generated-id_codeEditor"
>
<p
class="euiText"
>
Press Enter to start editing.
</p>
<p
class="euiText"
>
When you're done, press Escape to stop editing.
</p>
</button>
`;
exports[`EuiCodeEditor is rendered 1`] = `
<div
class="euiCodeEditorWrapper"
data-test-subj="test subject string"
>
<button
class="euiCodeEditorKeyboardHint"
data-test-subj="codeEditorHint"
id="generated-id_codeEditor"
>
<p
class="euiText"
>
Press Enter to start editing.
</p>
<p
class="euiText"
>
When you're done, press Escape to stop editing.
</p>
</button>
<div
class=" ace_editor ace-tm testClass1 testClass2"
id="generated-id"
style="width: 500px; height: 500px;"
>
<textarea
aria-label="aria-label"
autocapitalize="off"
autocorrect="off"
class="ace_text-input"
spellcheck="false"
style="opacity: 0;"
tabindex="-1"
wrap="off"
/>
<div
aria-hidden="true"
class="ace_gutter"
>
<div
class="ace_layer ace_gutter-layer ace_folding-enabled"
/>
<div
class="ace_gutter-active-line"
/>
</div>
<div
class="ace_scroller"
>
<div
class="ace_content"
>
<div
class="ace_layer ace_print-margin-layer"
>
<div
class="ace_print-margin"
style="left: 4px; visibility: visible;"
/>
</div>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_text-layer"
style="padding: 0px 4px;"
/>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_cursor-layer ace_hidden-cursors"
>
<div
class="ace_cursor"
/>
</div>
</div>
</div>
<div
class="ace_scrollbar ace_scrollbar-v"
style="display: none; width: 20px;"
>
<div
class="ace_scrollbar-inner"
style="width: 20px;"
/>
</div>
<div
class="ace_scrollbar ace_scrollbar-h"
style="display: none; height: 20px;"
>
<div
class="ace_scrollbar-inner"
style="height: 20px;"
/>
</div>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: hidden;"
>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
/>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
>
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
</div>
</div>
</div>
</div>
`;
exports[`EuiCodeEditor props aria attributes allows setting aria-describedby on textbox 1`] = `
<div
class="euiCodeEditorWrapper"
data-test-subj="codeEditorContainer"
>
<button
class="euiCodeEditorKeyboardHint"
data-test-subj="codeEditorHint"
id="generated-id_codeEditor"
>
<p
class="euiText"
>
Press Enter to start editing.
</p>
<p
class="euiText"
>
When you're done, press Escape to stop editing.
</p>
</button>
<div
class=" ace_editor ace-tm"
id="generated-id"
style="width: 500px; height: 500px;"
>
<textarea
aria-describedby="describedbyid"
autocapitalize="off"
autocorrect="off"
class="ace_text-input"
spellcheck="false"
style="opacity: 0;"
tabindex="-1"
wrap="off"
/>
<div
aria-hidden="true"
class="ace_gutter"
>
<div
class="ace_layer ace_gutter-layer ace_folding-enabled"
/>
<div
class="ace_gutter-active-line"
/>
</div>
<div
class="ace_scroller"
>
<div
class="ace_content"
>
<div
class="ace_layer ace_print-margin-layer"
>
<div
class="ace_print-margin"
style="left: 4px; visibility: visible;"
/>
</div>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_text-layer"
style="padding: 0px 4px;"
/>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_cursor-layer ace_hidden-cursors"
>
<div
class="ace_cursor"
/>
</div>
</div>
</div>
<div
class="ace_scrollbar ace_scrollbar-v"
style="display: none; width: 20px;"
>
<div
class="ace_scrollbar-inner"
style="width: 20px;"
/>
</div>
<div
class="ace_scrollbar ace_scrollbar-h"
style="display: none; height: 20px;"
>
<div
class="ace_scrollbar-inner"
style="height: 20px;"
/>
</div>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: hidden;"
>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
/>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
>
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
</div>
</div>
</div>
</div>
`;
exports[`EuiCodeEditor props aria attributes allows setting aria-labelledby on textbox 1`] = `
<div
class="euiCodeEditorWrapper"
data-test-subj="codeEditorContainer"
>
<button
class="euiCodeEditorKeyboardHint"
data-test-subj="codeEditorHint"
id="generated-id_codeEditor"
>
<p
class="euiText"
>
Press Enter to start editing.
</p>
<p
class="euiText"
>
When you're done, press Escape to stop editing.
</p>
</button>
<div
class=" ace_editor ace-tm"
id="generated-id"
style="width: 500px; height: 500px;"
>
<textarea
aria-labelledby="labelledbyid"
autocapitalize="off"
autocorrect="off"
class="ace_text-input"
spellcheck="false"
style="opacity: 0;"
tabindex="-1"
wrap="off"
/>
<div
aria-hidden="true"
class="ace_gutter"
>
<div
class="ace_layer ace_gutter-layer ace_folding-enabled"
/>
<div
class="ace_gutter-active-line"
/>
</div>
<div
class="ace_scroller"
>
<div
class="ace_content"
>
<div
class="ace_layer ace_print-margin-layer"
>
<div
class="ace_print-margin"
style="left: 4px; visibility: visible;"
/>
</div>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_text-layer"
style="padding: 0px 4px;"
/>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_cursor-layer ace_hidden-cursors"
>
<div
class="ace_cursor"
/>
</div>
</div>
</div>
<div
class="ace_scrollbar ace_scrollbar-v"
style="display: none; width: 20px;"
>
<div
class="ace_scrollbar-inner"
style="width: 20px;"
/>
</div>
<div
class="ace_scrollbar ace_scrollbar-h"
style="display: none; height: 20px;"
>
<div
class="ace_scrollbar-inner"
style="height: 20px;"
/>
</div>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: hidden;"
>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
/>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
>
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
</div>
</div>
</div>
</div>
`;
exports[`EuiCodeEditor props isReadOnly renders alternate hint text 1`] = `
<div
class="euiCodeEditorWrapper"
data-test-subj="codeEditorContainer"
>
<button
class="euiCodeEditorKeyboardHint"
data-test-subj="codeEditorHint"
id="generated-id_codeEditor"
>
<p
class="euiText"
>
Press Enter to start interacting with the code.
</p>
<p
class="euiText"
>
When you're done, press Escape to stop interacting with the code.
</p>
</button>
<div
class=" ace_editor ace-tm"
id="generated-id"
style="width: 500px; height: 500px;"
>
<textarea
autocapitalize="off"
autocorrect="off"
class="ace_text-input"
spellcheck="false"
style="opacity: 0;"
tabindex="-1"
wrap="off"
/>
<div
aria-hidden="true"
class="ace_gutter"
>
<div
class="ace_layer ace_gutter-layer ace_folding-enabled"
/>
<div
class="ace_gutter-active-line"
style="display: none;"
/>
</div>
<div
class="ace_scroller"
>
<div
class="ace_content"
>
<div
class="ace_layer ace_print-margin-layer"
>
<div
class="ace_print-margin"
style="left: 4px; visibility: visible;"
/>
</div>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_text-layer"
style="padding: 0px 4px;"
/>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_cursor-layer ace_hidden-cursors"
>
<div
class="ace_cursor"
/>
</div>
</div>
</div>
<div
class="ace_scrollbar ace_scrollbar-v"
style="display: none; width: 20px;"
>
<div
class="ace_scrollbar-inner"
style="width: 20px;"
/>
</div>
<div
class="ace_scrollbar ace_scrollbar-h"
style="display: none; height: 20px;"
>
<div
class="ace_scrollbar-inner"
style="height: 20px;"
/>
</div>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: hidden;"
>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
/>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
>
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
</div>
</div>
</div>
</div>
`;
exports[`EuiCodeEditor props theme renders terminal theme 1`] = `
<div
class="euiCodeEditorWrapper"
data-test-subj="codeEditorContainer"
>
<button
class="euiCodeEditorKeyboardHint"
data-test-subj="codeEditorHint"
id="generated-id_codeEditor"
>
<p
class="euiText"
>
Press Enter to start editing.
</p>
<p
class="euiText"
>
When you're done, press Escape to stop editing.
</p>
</button>
<div
class=" ace_editor ace-tm"
id="generated-id"
style="width: 500px; height: 500px;"
>
<textarea
autocapitalize="off"
autocorrect="off"
class="ace_text-input"
spellcheck="false"
style="opacity: 0;"
tabindex="-1"
wrap="off"
/>
<div
aria-hidden="true"
class="ace_gutter"
>
<div
class="ace_layer ace_gutter-layer ace_folding-enabled"
/>
<div
class="ace_gutter-active-line"
/>
</div>
<div
class="ace_scroller"
>
<div
class="ace_content"
>
<div
class="ace_layer ace_print-margin-layer"
>
<div
class="ace_print-margin"
style="left: 4px; visibility: visible;"
/>
</div>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_text-layer"
style="padding: 0px 4px;"
/>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_cursor-layer ace_hidden-cursors"
>
<div
class="ace_cursor"
/>
</div>
</div>
</div>
<div
class="ace_scrollbar ace_scrollbar-v"
style="display: none; width: 20px;"
>
<div
class="ace_scrollbar-inner"
style="width: 20px;"
/>
</div>
<div
class="ace_scrollbar ace_scrollbar-h"
style="display: none; height: 20px;"
>
<div
class="ace_scrollbar-inner"
style="height: 20px;"
/>
</div>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: hidden;"
>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
/>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
>
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,38 @@
.euiCodeEditorWrapper {
position: relative;
.ace_hidden-cursors {
opacity: 0;
}
&.euiCodeEditorWrapper-isEditing {
.ace_hidden-cursors {
opacity: 1;
}
}
}
.euiCodeEditorKeyboardHint {
position: absolute;
top: 0;
left: 0;
background: transparentize($euiColorGhost, .3);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
opacity: 0;
cursor: pointer;
height: 100%;
width: 100%;
&:focus {
opacity: 1;
border: 2px solid $euiColorPrimary;
z-index: $euiZLevel1;
}
&.euiCodeEditorKeyboardHint-isInactive {
display: none;
}
}

View file

@ -0,0 +1 @@
@import 'code_editor';

View file

@ -0,0 +1,117 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import EuiCodeEditor from './code_editor';
// @ts-ignore
import { keys } from '@elastic/eui/lib/services';
import { findTestSubject, requiredProps, takeMountedSnapshot } from '@elastic/eui/lib/test';
describe('EuiCodeEditor', () => {
test('is rendered', () => {
const component = mount(<EuiCodeEditor {...requiredProps} />);
expect(takeMountedSnapshot(component)).toMatchSnapshot();
});
describe('props', () => {
describe('isReadOnly', () => {
test('renders alternate hint text', () => {
const component = mount(<EuiCodeEditor isReadOnly />);
expect(takeMountedSnapshot(component)).toMatchSnapshot();
});
});
describe('theme', () => {
test('renders terminal theme', () => {
const component = mount(<EuiCodeEditor theme="terminal" />);
expect(takeMountedSnapshot(component)).toMatchSnapshot();
});
});
describe('aria attributes', () => {
test('allows setting aria-labelledby on textbox', () => {
const component = mount(<EuiCodeEditor aria-labelledby="labelledbyid" />);
expect(takeMountedSnapshot(component)).toMatchSnapshot();
});
test('allows setting aria-describedby on textbox', () => {
const component = mount(<EuiCodeEditor aria-describedby="describedbyid" />);
expect(takeMountedSnapshot(component)).toMatchSnapshot();
});
});
});
describe('behavior', () => {
let component: ReactWrapper;
beforeEach(() => {
// Addresses problems with attaching to document.body.
// https://meganesulli.com/blog/managing-focus-with-react-and-jest/
const container = document.createElement('div');
document.body.appendChild(container);
// We need to manually attach the element to document.body to assert against
// document.activeElement in our focus behavior tests, below.
component = mount(<EuiCodeEditor />, { attachTo: container });
});
afterEach(() => {
// We need to clean up after ourselves per https://github.com/enzymejs/enzyme/issues/2337.
if (component) {
component.unmount();
}
});
describe('hint element', () => {
test('should be tabable', () => {
const hint = findTestSubject(component, 'codeEditorHint').getDOMNode();
expect(hint).toMatchSnapshot();
});
test('should be disabled when the ui ace box gains focus', () => {
const hint = findTestSubject(component, 'codeEditorHint');
hint.simulate('keyup', { key: keys.ENTER });
expect(findTestSubject(component, 'codeEditorHint').getDOMNode()).toMatchSnapshot();
});
test('should be enabled when the ui ace box loses focus', () => {
const hint = findTestSubject(component, 'codeEditorHint');
hint.simulate('keyup', { key: keys.ENTER });
// @ts-ignore onBlurAce is known to exist and its params are only passed through to the onBlur callback
component.instance().onBlurAce();
expect(findTestSubject(component, 'codeEditorHint').getDOMNode()).toMatchSnapshot();
});
});
describe('interaction', () => {
test('bluring the ace textbox should call a passed onBlur prop', () => {
const blurSpy = jest.fn().mockName('blurSpy');
const el = mount(<EuiCodeEditor onBlur={blurSpy} />);
// @ts-ignore onBlurAce is known to exist and its params are only passed through to the onBlur callback
el.instance().onBlurAce();
expect(blurSpy).toHaveBeenCalled();
});
test('pressing escape in ace textbox will enable overlay', () => {
// We cannot simulate the `commands` path, but this interaction still
// serves as a fallback in cases where `commands` is unavailable.
// @ts-ignore onFocusAce is known to exist
component.instance().onFocusAce();
// @ts-ignore onKeydownAce is known to exist and its params' values are unimportant
component.instance().onKeydownAce({
preventDefault: () => {},
stopPropagation: () => {},
key: keys.ESCAPE,
});
const hint = findTestSubject(component, 'codeEditorHint').getDOMNode();
expect(hint).toBe(document.activeElement);
});
});
});
});

View file

@ -0,0 +1,308 @@
/*
* 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, { Component, AriaAttributes } from 'react';
import classNames from 'classnames';
import AceEditor, { IAceEditorProps } from 'react-ace';
import { EuiI18n } from '@elastic/eui';
// @ts-ignore
import { htmlIdGenerator, keys } from '@elastic/eui/lib/services';
import './_index.scss';
/**
* Wraps Object.keys with proper typescript definition of the resulting array
*/
function keysOf<T, K extends keyof T>(obj: T): K[] {
return Object.keys(obj) as K[];
}
const DEFAULT_MODE = 'text';
const DEFAULT_THEME = 'textmate';
function setOrRemoveAttribute(
element: HTMLTextAreaElement,
attributeName: SupportedAriaAttribute,
value: SupportedAriaAttributes[SupportedAriaAttribute]
) {
if (value === null || value === undefined) {
element.removeAttribute(attributeName);
} else {
element.setAttribute(attributeName, value);
}
}
type SupportedAriaAttribute = 'aria-label' | 'aria-labelledby' | 'aria-describedby';
type SupportedAriaAttributes = Pick<AriaAttributes, SupportedAriaAttribute>;
export interface EuiCodeEditorProps extends SupportedAriaAttributes, Omit<IAceEditorProps, 'mode'> {
width?: string;
height?: string;
onBlur?: IAceEditorProps['onBlur'];
onFocus?: IAceEditorProps['onFocus'];
isReadOnly?: boolean;
setOptions: IAceEditorProps['setOptions'];
cursorStart?: number;
'data-test-subj'?: string;
/**
* Select the `brace` theme
* The matching theme file must also be imported from `brace` (e.g., `import 'brace/theme/github';`)
*/
theme?: IAceEditorProps['theme'];
/**
* Use string for a built-in mode or object for a custom mode
*/
mode?: IAceEditorProps['mode'] | object;
id?: string;
}
export interface EuiCodeEditorState {
isHintActive: boolean;
isEditing: boolean;
name: string;
}
class EuiCodeEditor extends Component<EuiCodeEditorProps, EuiCodeEditorState> {
static defaultProps = {
setOptions: {},
};
state: EuiCodeEditorState = {
isHintActive: true,
isEditing: false,
name: htmlIdGenerator()(),
};
constructor(props: EuiCodeEditorProps) {
super(props);
}
idGenerator = htmlIdGenerator();
aceEditor: AceEditor | null = null;
editorHint: HTMLButtonElement | null = null;
aceEditorRef = (aceEditor: AceEditor | null) => {
if (aceEditor) {
this.aceEditor = aceEditor;
const textbox = aceEditor.editor.textInput.getElement() as HTMLTextAreaElement;
textbox.tabIndex = -1;
textbox.addEventListener('keydown', this.onKeydownAce);
setOrRemoveAttribute(textbox, 'aria-label', this.props['aria-label']);
setOrRemoveAttribute(textbox, 'aria-labelledby', this.props['aria-labelledby']);
setOrRemoveAttribute(textbox, 'aria-describedby', this.props['aria-describedby']);
}
};
onEscToExit = () => {
this.stopEditing();
if (this.editorHint) {
this.editorHint.focus();
}
};
onKeydownAce = (event: KeyboardEvent) => {
if (event.key === keys.ESCAPE) {
event.preventDefault();
event.stopPropagation();
// Handles exiting edit mode when `isReadOnly` is set.
// Other 'esc' cases handled by `stopEditingOnEsc` command.
// Would run after `stopEditingOnEsc`.
if (this.aceEditor !== null && !this.aceEditor.editor.completer && this.state.isEditing) {
this.onEscToExit();
}
}
};
onFocusAce: IAceEditorProps['onFocus'] = (event, editor) => {
this.setState({
isEditing: true,
});
if (this.props.onFocus) {
this.props.onFocus(event, editor);
}
};
onBlurAce: IAceEditorProps['onBlur'] = (event, editor) => {
this.stopEditing();
if (this.props.onBlur) {
this.props.onBlur(event, editor);
}
};
startEditing = () => {
this.setState({
isHintActive: false,
});
if (this.aceEditor !== null) {
this.aceEditor.editor.textInput.focus();
}
};
stopEditing() {
this.setState({
isHintActive: true,
isEditing: false,
});
}
isCustomMode() {
return typeof this.props.mode === 'object';
}
setCustomMode() {
if (this.aceEditor !== null) {
this.aceEditor.editor.getSession().setMode(this.props.mode);
}
}
componentDidMount() {
if (this.isCustomMode()) {
this.setCustomMode();
}
const { isReadOnly, id } = this.props;
const textareaProps: {
id?: string;
readOnly?: boolean;
} = { id, readOnly: isReadOnly };
const el = document.getElementById(this.state.name);
if (el) {
const textarea = el.querySelector('textarea');
if (textarea)
keysOf(textareaProps).forEach((key) => {
if (textareaProps[key]) textarea.setAttribute(`${key}`, textareaProps[key]!.toString());
});
}
}
componentDidUpdate(prevProps: EuiCodeEditorProps) {
if (this.props.mode !== prevProps.mode && this.isCustomMode()) {
this.setCustomMode();
}
}
render() {
const {
width,
height,
onBlur,
isReadOnly,
setOptions,
cursorStart,
mode = DEFAULT_MODE,
'data-test-subj': dataTestSubj = 'codeEditorContainer',
theme = DEFAULT_THEME,
commands = [],
...rest
} = this.props;
const classes = classNames('euiCodeEditorWrapper', {
'euiCodeEditorWrapper-isEditing': this.state.isEditing,
});
const promptClasses = classNames('euiCodeEditorKeyboardHint', {
'euiCodeEditorKeyboardHint-isInactive': !this.state.isHintActive,
});
let filteredCursorStart;
const options: IAceEditorProps['setOptions'] = { ...setOptions };
if (isReadOnly) {
// Put the cursor at the beginning of the editor, so that it doesn't look like
// a prompt to begin typing.
filteredCursorStart = -1;
Object.assign(options, {
readOnly: true,
highlightActiveLine: false,
highlightGutterLine: false,
});
} else {
filteredCursorStart = cursorStart;
}
const prompt = (
<button
className={promptClasses}
id={this.idGenerator('codeEditor')}
ref={(hint) => {
this.editorHint = hint;
}}
onClick={this.startEditing}
data-test-subj="codeEditorHint"
>
<p className="euiText">
{isReadOnly ? (
<EuiI18n
token="euiCodeEditor.startInteracting"
default="Press Enter to start interacting with the code."
/>
) : (
<EuiI18n token="euiCodeEditor.startEditing" default="Press Enter to start editing." />
)}
</p>
<p className="euiText">
{isReadOnly ? (
<EuiI18n
token="euiCodeEditor.stopInteracting"
default="When you're done, press Escape to stop interacting with the code."
/>
) : (
<EuiI18n
token="euiCodeEditor.stopEditing"
default="When you're done, press Escape to stop editing."
/>
)}
</p>
</button>
);
return (
<div className={classes} style={{ width, height }} data-test-subj={dataTestSubj}>
{prompt}
<AceEditor
// Setting a default, existing `mode` is necessary to properly initialize the editor
// prior to dynamically setting a custom mode (https://github.com/elastic/eui/pull/2616)
mode={this.isCustomMode() ? DEFAULT_MODE : (mode as string)} // https://github.com/securingsincity/react-ace/pull/771
name={this.state.name}
theme={theme}
ref={this.aceEditorRef}
width={width}
height={height}
onFocus={this.onFocusAce}
onBlur={this.onBlurAce}
setOptions={options}
editorProps={{
$blockScrolling: Infinity,
}}
cursorStart={filteredCursorStart}
commands={[
// Handles exiting edit mode in all cases except `isReadOnly`
// Runs before `onKeydownAce`.
{
name: 'stopEditingOnEsc',
bindKey: { win: 'Esc', mac: 'Esc' },
exec: this.onEscToExit,
},
...commands,
]}
{...rest}
/>
</div>
);
}
}
// Needed for React.lazy
// eslint-disable-next-line import/no-default-export
export default EuiCodeEditor;

View file

@ -0,0 +1,32 @@
/*
* 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 { EuiLoadingContentProps, EuiLoadingContent } from '@elastic/eui';
import type { EuiCodeEditorProps } from './code_editor';
const Placeholder = ({ height }: { height?: string }) => {
const numericalHeight = height ? parseInt(height, 10) : 0;
// The height of one EuiLoadingContent line is 24px.
const lineHeight = 24;
const calculatedLineCount =
numericalHeight < lineHeight ? 1 : Math.floor(numericalHeight / lineHeight);
const lines = Math.min(10, calculatedLineCount);
return <EuiLoadingContent lines={lines as EuiLoadingContentProps['lines']} />;
};
const LazyEuiCodeEditor = React.lazy(() => import('./code_editor'));
export const EuiCodeEditor = (props: EuiCodeEditorProps) => (
<React.Suspense fallback={<Placeholder height={props.height} />}>
<LazyEuiCodeEditor {...props} />
</React.Suspense>
);
export type { EuiCodeEditorProps } from './code_editor';

View file

@ -0,0 +1,32 @@
/*
* 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';
// NOTE: Import this file for its side-effects. You must import it before the code that it mocks
// is imported. Typically this means just importing above your other imports.
// See https://jestjs.io/docs/manual-mocks for more info.
// This mocks any direct imports of EuiCodeEditor, e.g. by JsonEditor.
jest.mock('.', () => {
const original = jest.requireActual('.');
return {
...original,
// Mock EuiCodeEditor, which uses React Ace under the hood.
EuiCodeEditor: (props: any) => (
<input
data-test-subj={props['data-test-subj'] || 'mockCodeEditor'}
data-currentvalue={props.value}
onChange={(syntheticEvent: any) => {
props.onChange(syntheticEvent.jsonString);
}}
/>
),
};
});

View file

@ -7,9 +7,10 @@
*/
import React, { useCallback, useMemo } from 'react';
import { EuiFormRow, EuiCodeEditor } from '@elastic/eui';
import { EuiFormRow } from '@elastic/eui';
import { debounce } from 'lodash';
import { EuiCodeEditor } from '../code_editor';
import { useJson, OnJsonEditorUpdateHandler } from './use_json';
interface Props<T extends object = { [key: string]: any }> {

View file

@ -20,6 +20,7 @@ export { JsonEditor, OnJsonEditorUpdateHandler, JsonEditorState } from './compon
export { PageLoading } from './components/page_loading';
export { SectionLoading } from './components/section_loading';
export { EuiCodeEditor, EuiCodeEditorProps } from './components/code_editor';
export { Frequency, CronEditor } from './components/cron_editor';
export {

View file

@ -11,6 +11,7 @@
"ui": true,
"configPath": ["xpack", "grokdebugger"],
"requiredBundles": [
"kibanaReact"
"kibanaReact",
"esUiShared"
]
}

View file

@ -6,17 +6,12 @@
*/
import React from 'react';
import {
EuiAccordion,
EuiCallOut,
EuiCodeBlock,
EuiFormRow,
EuiCodeEditor,
EuiSpacer,
} from '@elastic/eui';
import { EDITOR } from '../../../common/constants';
import { EuiAccordion, EuiCallOut, EuiCodeBlock, EuiFormRow, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EDITOR } from '../../../common/constants';
import { EuiCodeEditor } from '../../shared_imports';
export function CustomPatternsInput({ value, onChange }) {
const sampleCustomPatterns = `POSTFIX_QUEUEID [0-9A-F]{10,11}
MSG message-id=<%{GREEDYDATA}>`;

View file

@ -6,10 +6,12 @@
*/
import React from 'react';
import { EuiFormRow, EuiCodeEditor } from '@elastic/eui';
import { EDITOR } from '../../../common/constants';
import { EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EDITOR } from '../../../common/constants';
import { EuiCodeEditor } from '../../shared_imports';
export function EventInput({ value, onChange }) {
return (
<EuiFormRow

View file

@ -6,9 +6,11 @@
*/
import React from 'react';
import { EuiFormRow, EuiCodeEditor } from '@elastic/eui';
import { EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCodeEditor } from '../../shared_imports';
export function EventOutput({ value }) {
return (
<EuiFormRow

View file

@ -6,11 +6,13 @@
*/
import React from 'react';
import { EuiFormRow, EuiCodeEditor } from '@elastic/eui';
import { EDITOR } from '../../../common/constants';
import { GrokMode } from '../../lib/ace';
import { EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EDITOR } from '../../../common/constants';
import { EuiCodeEditor } from '../../shared_imports';
import { GrokMode } from '../../lib/ace';
export function PatternInput({ value, onChange }) {
return (
<EuiFormRow

View file

@ -0,0 +1,8 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { EuiCodeEditor } from '../../../../src/plugins/es_ui_shared/public';

View file

@ -8,6 +8,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import '../../../../../../../../../src/plugins/es_ui_shared/public/components/code_editor/jest_mock';
import { setupEnvironment } from './helpers';
import { setup, ComponentTemplateCreateTestBed } from './helpers/component_template_create.helpers';
@ -74,7 +75,7 @@ describe('<ComponentTemplateCreate />', () => {
// Meta editor should be hidden by default
// Since the editor itself is mocked, we checked for the mocked element
expect(exists('mockCodeEditor')).toBe(false);
expect(exists('metaEditor')).toBe(false);
await act(async () => {
actions.toggleMetaSwitch();
@ -82,7 +83,7 @@ describe('<ComponentTemplateCreate />', () => {
component.update();
expect(exists('mockCodeEditor')).toBe(true);
expect(exists('metaEditor')).toBe(true);
});
describe('Validation', () => {

View file

@ -276,7 +276,7 @@ const createActions = (testBed: TestBed<TestSubjects>) => {
};
const updateJsonEditor = (testSubject: TestSubjects, value: object) => {
find(testSubject).simulate('change', { jsonContent: JSON.stringify(value) });
find(testSubject).simulate('change', { jsonString: JSON.stringify(value) });
};
const getJsonEditorValue = (testSubject: TestSubjects) => {

View file

@ -6,6 +6,8 @@
*/
import React from 'react';
/* eslint-disable-next-line @kbn/eslint/no-restricted-paths */
import '../../../../../../../../../../src/plugins/es_ui_shared/public/components/code_editor/jest_mock';
import { GlobalFlyout } from '../../../../../../../../../../src/plugins/es_ui_shared/public';
import {
docLinksServiceMock,
@ -30,16 +32,6 @@ jest.mock('@elastic/eui', () => {
}}
/>
),
// Mocking EuiCodeEditor, which uses React Ace under the hood
EuiCodeEditor: (props: any) => (
<input
data-test-subj={props['data-test-subj'] || 'mockCodeEditor'}
data-currentvalue={props.value}
onChange={(e: any) => {
props.onChange(e.jsonContent);
}}
/>
),
// Mocking EuiSuperSelect to be able to easily change its value
// with a `myWrapper.simulate('change', { target: { value: 'someValue' } })`
EuiSuperSelect: (props: any) => (

View file

@ -7,23 +7,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
// Mocking EuiCodeEditor, which uses React Ace under the hood
EuiCodeEditor: (props: any) => (
<input
data-test-subj="mockCodeEditor"
onChange={(syntheticEvent: any) => {
props.onChange(syntheticEvent.jsonString);
}}
/>
),
};
});
import '../../../../../../../../../src/plugins/es_ui_shared/public/components/code_editor/jest_mock';
jest.mock('lodash', () => {
const original = jest.requireActual('lodash');

View file

@ -10,11 +10,14 @@ import React from 'react';
import axios from 'axios';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
/* eslint-disable @kbn/eslint/no-restricted-paths */
/* eslint-disable-next-line @kbn/eslint/no-restricted-paths */
import { usageCollectionPluginMock } from 'src/plugins/usage_collection/public/mocks';
import { registerTestBed, TestBed } from '@kbn/test/jest';
import { stubWebWorker } from '@kbn/test/jest';
/* eslint-disable-next-line @kbn/eslint/no-restricted-paths */
import '../../../../../../../../src/plugins/es_ui_shared/public/components/code_editor/jest_mock';
import { uiMetricService, apiService } from '../../../services';
import { Props } from '../';
import { initHttpRequests } from './http_requests.helpers';
@ -24,6 +27,7 @@ stubWebWorker();
jest.mock('../../../../../../../../src/plugins/kibana_react/public', () => {
const original = jest.requireActual('../../../../../../../../src/plugins/kibana_react/public');
return {
...original,
// Mocking CodeEditor, which uses React Monaco under the hood
@ -39,22 +43,6 @@ jest.mock('../../../../../../../../src/plugins/kibana_react/public', () => {
};
});
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
// Mocking EuiCodeEditor, which uses React Ace under the hood
EuiCodeEditor: (props: any) => (
<input
data-test-subj={props['data-test-subj']}
onChange={(syntheticEvent: any) => {
props.onChange(syntheticEvent.jsonString);
}}
/>
),
};
});
jest.mock('react-virtualized', () => {
const original = jest.requireActual('react-virtualized');

View file

@ -6,25 +6,9 @@
*/
import React from 'react';
import '../../../../../../../../../src/plugins/es_ui_shared/public/components/code_editor/jest_mock';
import { ModalProvider, OnDoneLoadJsonHandler } from './modal_provider';
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
// Mocking EuiCodeEditor, which uses React Ace under the hood
EuiCodeEditor: (props: any) => (
<input
data-test-subj="mockCodeEditor"
onChange={(syntheticEvent: any) => {
props.onChange(syntheticEvent.jsonString);
}}
/>
),
};
});
jest.mock('lodash', () => {
const original = jest.requireActual('lodash');

View file

@ -8500,7 +8500,7 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace@0.11.1, brace@^0.11.0, brace@^0.11.1:
brace@0.11.1, brace@^0.11.1:
version "0.11.1"
resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58"
integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg=
@ -18684,7 +18684,7 @@ lodash.isempty@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
integrity sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=
lodash.isequal@^4.0.0, lodash.isequal@^4.1.1, lodash.isequal@^4.5.0:
lodash.isequal@^4.0.0, lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
@ -22865,16 +22865,6 @@ re2@^1.15.4:
nan "^2.14.1"
node-gyp "^7.0.0"
react-ace@^5.9.0:
version "5.9.0"
resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-5.9.0.tgz#427a1cc4869b960a6f9748aa7eb169a9269fc336"
integrity sha512-r6Tuce6seG05g9kT2Tio6DWohy06knG7e5u9OfhvMquZL+Cyu4eqPf60K1Vi2RXlS3+FWrdG8Rinwu4+oQjjgw==
dependencies:
brace "^0.11.0"
lodash.get "^4.4.2"
lodash.isequal "^4.1.1"
prop-types "^15.5.8"
react-ace@^7.0.5:
version "7.0.5"
resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-7.0.5.tgz#798299fd52ddf3a3dcc92afc5865538463544f01"