Consolidate <CodeEditor/> (#170313)

## Summary

Fix https://github.com/elastic/kibana/issues/159719

- Remove duplicate of code_editor code from `kibana_react` and apply
recent changes to the version in `packages/`
- Fix code_editor styles in `packages/`
https://github.com/elastic/kibana/pull/170313#discussion_r1378839369
- Revert setting default height to 100px (as it breaks in some places)
https://github.com/elastic/kibana/pull/170313#discussion_r1378838788


### Risks

Ideally we should smoke check the code editor in all the places, I
checked bunch of them.
As of special custom features, I tested: 
- The theme switch
- The placeholder 
- The a11y hint
- Fullscreen mode
This commit is contained in:
Anton Dosov 2023-11-03 17:30:58 +01:00 committed by GitHub
parent 30c859206c
commit 3249c1a116
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
84 changed files with 152 additions and 3259 deletions

4
.github/CODEOWNERS vendored
View file

@ -78,9 +78,7 @@ x-pack/test/cloud_integration/plugins/saml_provider @elastic/kibana-core
x-pack/plugins/cloud_integrations/cloud_links @elastic/kibana-core
x-pack/plugins/cloud @elastic/kibana-core
x-pack/plugins/cloud_security_posture @elastic/kibana-cloud-security-posture
packages/shared-ux/code_editor/impl @elastic/appex-sharedux
packages/shared-ux/code_editor/mocks @elastic/appex-sharedux
packages/shared-ux/code_editor/types @elastic/appex-sharedux
packages/shared-ux/code_editor @elastic/appex-sharedux
packages/kbn-coloring @elastic/kibana-visualizations
packages/kbn-config @elastic/kibana-core
packages/kbn-config-mocks @elastic/kibana-core

View file

@ -184,9 +184,7 @@
"@kbn/cloud-links-plugin": "link:x-pack/plugins/cloud_integrations/cloud_links",
"@kbn/cloud-plugin": "link:x-pack/plugins/cloud",
"@kbn/cloud-security-posture-plugin": "link:x-pack/plugins/cloud_security_posture",
"@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/code-editor": "link:packages/shared-ux/code_editor",
"@kbn/coloring": "link:packages/kbn-coloring",
"@kbn/config": "link:packages/kbn-config",
"@kbn/config-mocks": "link:packages/kbn-config-mocks",

View file

@ -3,8 +3,8 @@
exports[`<CodeEditor /> hint element should be tabable 1`] = `
<div
aria-label="Code Editor"
class="kibanaCodeEditor__keyboardHint"
data-test-subj="codeEditorHint"
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).,false"
data-test-subj="codeEditorHint codeEditorHint--active"
id="1234"
role="button"
tabindex="0"
@ -135,7 +135,7 @@ exports[`<CodeEditor /> is rendered 1`] = `
<p>
<FormattedMessage
defaultMessage="Press {key} to start editing."
id="kibana-react.kibanaCodeEditor.startEditing"
id="sharedUXPackages.codeEditor.startEditing"
values={
Object {
"key": <strong>
@ -148,7 +148,7 @@ exports[`<CodeEditor /> is rendered 1`] = `
<p>
<FormattedMessage
defaultMessage="Press {key} to stop editing."
id="kibana-react.kibanaCodeEditor.stopEditing"
id="sharedUXPackages.codeEditor.stopEditing"
values={
Object {
"key": <strong>
@ -255,8 +255,28 @@ exports[`<CodeEditor /> is rendered 1`] = `
>
<div
aria-label="Code Editor"
className="kibanaCodeEditor__keyboardHint"
data-test-subj="codeEditorHint"
css={
Array [
Object {
"map": undefined,
"name": "jym74u",
"next": undefined,
"styles": "
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
&:focus {
z-index: 6000;
}
",
"toString": [Function],
},
false,
]
}
data-test-subj="codeEditorHint codeEditorHint--active"
id="1234"
onBlur={[Function]}
onClick={[Function]}

View file

@ -9,10 +9,10 @@
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 { CodeEditorStorybookMock, CodeEditorStorybookParams } from './mocks/storybook';
import mdx from './README.mdx';
import { CodeEditor } from './code_editor';

View file

@ -133,16 +133,20 @@ describe('<CodeEditor />', () => {
test('should be tabable', () => {
const DOMnode = getHint().getDOMNode();
expect(getHint().find('[data-test-subj="codeEditorHint"]').exists()).toBeTruthy();
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();
expect(getHint().find('[data-test-subj~="codeEditorHint"]').prop('data-test-subj')).toContain(
`codeEditorHint--active`
);
getHint().simulate('keydown', { key: keys.ENTER });
expect(getHint().find('[data-test-subj="codeEditorHint"]').exists()).toBeFalsy();
expect(getHint().find('[data-test-subj~="codeEditorHint"]').prop('data-test-subj')).toContain(
`codeEditorHint--inactive`
);
});
test('should be enabled when hitting the ESC key', () => {
@ -152,7 +156,9 @@ describe('<CodeEditor />', () => {
keyCode: monaco.KeyCode.Escape,
});
// expect((getHint().props() as any).className).not.toContain('isInactive');
expect(getHint().find('[data-test-subj~="codeEditorHint"]').prop('data-test-subj')).toContain(
`codeEditorHint--active`
);
});
test('should detect that the suggestion menu is open and not show the hint on ESC', async () => {

View file

@ -25,23 +25,20 @@ import {
import { monaco } from '@kbn/monaco';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import './register_languages';
import { remeasureFonts } from './remeasure_fonts';
import { PlaceholderWidget } from './placeholder_widget';
import {
codeEditorControlsStyles,
codeEditorControlsWithinFullScreenStyles,
codeEditorFullScreenStyles,
codeEditorKeyboardHintStyles,
codeEditorStyles,
styles,
DARK_THEME,
LIGHT_THEME,
DARK_THEME_TRANSPARENT,
LIGHT_THEME_TRANSPARENT,
} from './editor.styles';
export interface Props {
export interface CodeEditorProps {
/** Width of editor. Defaults to 100%. */
width?: string | number;
@ -132,12 +129,12 @@ export interface Props {
allowFullScreen?: boolean;
}
export const CodeEditor: React.FC<Props> = ({
export const CodeEditor: React.FC<CodeEditorProps> = ({
languageId,
value,
onChange,
width,
height = '100px',
height,
options,
overrideEditorWillMount,
editorDidMount,
@ -182,12 +179,6 @@ export const CodeEditor: React.FC<Props> = ({
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();
@ -269,14 +260,12 @@ export const CodeEditor: React.FC<Props> = ({
<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 }}
@ -286,14 +275,12 @@ export const CodeEditor: React.FC<Props> = ({
<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 }}
@ -304,7 +291,13 @@ export const CodeEditor: React.FC<Props> = ({
}
>
<div
css={promptClasses}
css={[
styles.keyboardHint(euiTheme),
!isHintActive &&
css`
display: none;
`,
]}
id={htmlIdGenerator('codeEditor')()}
ref={editorHint}
tabIndex={0}
@ -312,19 +305,11 @@ export const CodeEditor: React.FC<Props> = ({
onClick={startEditing}
onKeyDown={onKeyDownHint}
aria-label={ariaLabel}
data-test-subj={isHintActive ? 'codeEditorHint' : 'codeEditor'}
data-test-subj={`codeEditorHint codeEditorHint--${isHintActive ? 'active' : 'inactive'}`}
/>
</EuiToolTip>
);
}, [
onKeyDownHint,
startEditing,
ariaLabel,
isReadOnly,
promptClasses,
defaultStyles,
isHintActive,
]);
}, [isHintActive, isReadOnly, euiTheme, startEditing, onKeyDownHint, ariaLabel]);
const _editorWillMount = useCallback(
(__monaco: unknown) => {
@ -357,6 +342,7 @@ export const CodeEditor: React.FC<Props> = ({
}
});
// Register themes
monaco.editor.defineTheme('euiColors', useDarkTheme ? DARK_THEME : LIGHT_THEME);
monaco.editor.defineTheme(
'euiColorsTransparent',
@ -433,35 +419,46 @@ export const CodeEditor: React.FC<Props> = ({
useEffect(() => {
if (placeholder && !value && _editor.current) {
// Mounts editor inside constructor
_placeholderWidget.current = new PlaceholderWidget(placeholder, _editor.current);
_placeholderWidget.current = new PlaceholderWidget(placeholder, euiTheme, _editor.current);
}
return () => {
_placeholderWidget.current?.dispose();
_placeholderWidget.current = null;
};
}, [placeholder, value]);
}, [placeholder, value, euiTheme]);
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
useEffect(() => {
// Register themes when 'useDarkThem' changes
monaco.editor.defineTheme('euiColors', useDarkTheme ? DARK_THEME : LIGHT_THEME);
return options?.theme ?? (transparentBackground ? 'euiColorsTransparent' : 'euiColors');
}, [useDarkTheme, transparentBackground, options]);
monaco.editor.defineTheme(
'euiColorsTransparent',
useDarkTheme ? DARK_THEME_TRANSPARENT : LIGHT_THEME_TRANSPARENT
);
}, [useDarkTheme]);
const theme = options?.theme ?? (transparentBackground ? 'euiColorsTransparent' : 'euiColors');
return (
<div css={codeEditorStyles()} onKeyDown={onKeyDown}>
<div
css={styles.container}
onKeyDown={onKeyDown}
data-test-subj="kibanaCodeEditor"
className="kibanaCodeEditor"
>
{renderPrompt()}
<FullScreenDisplay>
{allowFullScreen || isCopyable ? (
<div css={controlStyles}>
<div
css={
isFullScreen
? [styles.controls.base(euiTheme), styles.controls.fullscreen(euiTheme)]
: styles.controls.base(euiTheme)
}
>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<CopyButton />
@ -478,7 +475,6 @@ export const CodeEditor: React.FC<Props> = ({
value={value}
onChange={onChange}
width={isFullScreen ? '100vw' : width}
// previously defaulted to height which defaulted to 100% but this makes it unviewable
height={isFullScreen ? '100vh' : height}
editorWillMount={_editorWillMount}
editorDidMount={_editorDidMount}
@ -539,7 +535,6 @@ const useFullScreen = ({ allowFullScreen }: { allowFullScreen?: boolean }) => {
>
{([fullscreenCollapse, fullscreenExpand]: string[]) => (
<EuiButtonIcon
css={[codeEditorStyles(), codeEditorFullScreenStyles]}
onClick={toggleFullScreen}
iconType={isFullScreen ? 'fullScreenExit' : 'fullScreen'}
color="text"
@ -551,8 +546,6 @@ const useFullScreen = ({ allowFullScreen }: { allowFullScreen?: boolean }) => {
);
};
const { euiTheme } = useEuiTheme();
const FullScreenDisplay = useMemo(
() =>
({ children }: { children: Array<JSX.Element | null> | JSX.Element }) => {
@ -561,20 +554,12 @@ const useFullScreen = ({ allowFullScreen }: { allowFullScreen?: boolean }) => {
return (
<EuiOverlayMask>
<EuiFocusTrap clickOutsideDisables={true}>
<div
css={[
codeEditorStyles(),
codeEditorFullScreenStyles(),
codeEditorControlsWithinFullScreenStyles(euiTheme.size.l),
]}
>
{children}
</div>
<div css={styles.fullscreenContainer}>{children}</div>
</EuiFocusTrap>
</EuiOverlayMask>
);
},
[isFullScreen, euiTheme]
[isFullScreen]
);
return {
@ -593,7 +578,7 @@ const useCopy = ({ isCopyable, value }: { isCopyable: boolean; value: string })
if (!showCopyButton) return null;
return (
<div css={codeEditorStyles()} className="euiCodeBlock__copyButton">
<div className="euiCodeBlock__copyButton">
<EuiI18n token="euiCodeBlock.copyButton" default="Copy">
{(copyButton: string) => (
<EuiCopy textToCopy={value}>

View file

@ -6,9 +6,44 @@
* Side Public License, v 1.
*/
import { css } from '@emotion/react';
import { monaco } from '@kbn/monaco';
import type { EuiThemeComputed } from '@elastic/eui';
import { euiDarkVars as darkTheme, euiLightVars as lightTheme } from '@kbn/ui-theme';
import { euiLightVars as lightTheme, euiDarkVars as darkTheme } from '@kbn/ui-theme';
export const styles = {
container: css`
position: relative;
height: 100%;
`,
fullscreenContainer: css`
position: absolute;
left: 0;
top: 0;
`,
keyboardHint: (euiTheme: EuiThemeComputed) => css`
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
&:focus {
z-index: ${euiTheme.levels.mask};
}
`,
controls: {
base: (euiTheme: EuiThemeComputed) => css`
position: absolute;
top: ${euiTheme.size.xs};
right: ${euiTheme.size.base};
z-index: ${euiTheme.levels.menu};
`,
fullscreen: (euiTheme: EuiThemeComputed) => css`
top: ${euiTheme.size.l};
right: ${euiTheme.size.l};
`,
},
};
// NOTE: For talk around where this theme information will ultimately live,
// please see this discuss issue: https://github.com/elastic/kibana/issues/43814

View file

@ -1,396 +0,0 @@
// 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={250}
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

@ -1,211 +0,0 @@
/*
* 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

@ -1,11 +0,0 @@
/*
* 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

@ -1,184 +0,0 @@
/*
* 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

@ -1,12 +0,0 @@
/*
* 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

@ -1,12 +0,0 @@
/*
* 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

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

@ -1,11 +0,0 @@
/*
* 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

@ -1,223 +0,0 @@
/*
* 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

@ -1,11 +0,0 @@
/*
* 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

@ -1,198 +0,0 @@
/*
* 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

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

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { CodeEditor } from './code_editor';
export { CodeEditor, type CodeEditorProps } from './code_editor';

View file

@ -8,6 +8,6 @@
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
rootDir: '../../..',
roots: ['<rootDir>/packages/shared-ux/code_editor'],
};

View file

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

View file

@ -1,9 +0,0 @@
/*
* 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

@ -1,13 +0,0 @@
/*
* 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

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

View file

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

View file

@ -6,8 +6,7 @@
* Side Public License, v 1.
*/
import { AbstractStorybookMock } from '@kbn/shared-ux-storybook-mock';
import type { Props as CodeEditorProps } from '@kbn/code-editor-types';
import type { CodeEditorProps } from '../code_editor';
type PropArguments = Pick<
CodeEditorProps,
@ -19,7 +18,7 @@ type PropArguments = Pick<
| 'placeholder'
>;
export type Params = Record<keyof PropArguments, any>;
export type CodeEditorStorybookParams = Record<keyof PropArguments, any>;
/**
* Storybook mock for the `CodeEditor` component
@ -74,7 +73,7 @@ export class CodeEditorStorybookMock extends AbstractStorybookMock<
serviceArguments = {};
dependencies = [];
getProps(params?: Params): CodeEditorProps {
getProps(params?: CodeEditorStorybookParams): CodeEditorProps {
return {
languageId: this.getArgumentValue('languageId', params),
value: this.getArgumentValue('value', params),

View file

@ -1,20 +0,0 @@
{
"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

@ -7,10 +7,13 @@
*/
import { monaco } from '@kbn/monaco';
import { css } from '@emotion/css';
import type { EuiThemeComputed } from '@elastic/eui';
export class PlaceholderWidget implements monaco.editor.IContentWidget {
constructor(
private readonly placeholderText: string,
private readonly euiTheme: EuiThemeComputed,
private readonly editor: monaco.editor.ICodeEditor
) {
editor.addContentWidget(this);
@ -26,7 +29,11 @@ export class PlaceholderWidget implements monaco.editor.IContentWidget {
if (!this.domNode) {
const domNode = document.createElement('div');
domNode.innerText = this.placeholderText;
domNode.className = 'kibanaCodeEditor__placeholderContainer';
domNode.className = css`
color: ${this.euiTheme.colors.subduedText};
width: max-content;
pointer-events: none;
`;
this.editor.applyFontInfo(domNode);
this.domNode = domNode;
}

View file

@ -1,5 +1,5 @@
{
"extends": "../../../../tsconfig.base.json",
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
@ -8,12 +8,12 @@
"react",
"@emotion/react/types/css-prop",
"@kbn/ambient-ui-types",
]
},
"include": [
"**/*.ts",
"**/*.tsx",
"../../../typings/**/*",
],
"exclude": [
"target/**/*"
@ -22,8 +22,8 @@
"@kbn/monaco",
"@kbn/i18n",
"@kbn/i18n-react",
"@kbn/code-editor-mocks",
"@kbn/ui-theme",
"@kbn/test-jest-helpers",
"@kbn/shared-ux-storybook-mock",
]
}

View file

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

View file

@ -1,101 +0,0 @@
/*
* 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

@ -1,13 +0,0 @@
/*
* 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

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

View file

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

View file

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

View file

@ -3,14 +3,10 @@
This re-usable code editor component was built as a layer of abstraction on top 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)). The goal of this component is to expose a set of the most-used, most-helpful features from Monaco in a way that's easy to use out of the box. If a use case requires additional features, this component still allows access to all other Monaco features.
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)
- [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
## Storybook Examples
To run the `CodeEditor` Storybook, from the root kibana directory, run `yarn storybook kibana_react`
All stories for the component live in `code_editor.examples.tsx`

View file

@ -1,276 +0,0 @@
/*
* 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 { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { monaco as monacoEditor } from '@kbn/monaco';
import { CodeEditor } from './code_editor';
// 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
`;
storiesOf('CodeEditor', module)
.addParameters({
info: {
// CodeEditor has no PropTypes set so this table will show up
// as blank. I'm just disabling it to reduce confusion
propTablesExclude: [CodeEditor],
},
})
.add(
'default',
() => (
<div>
<CodeEditor
languageId="plaintext"
height={250}
value="Hello!"
onChange={action('onChange')}
/>
</div>
),
{
info: {
text: 'Plaintext Monaco Editor',
},
}
)
.add(
'dark mode',
() => (
<div>
<CodeEditor
languageId="plaintext"
height={250}
value="Hello!"
onChange={action('onChange')}
useDarkTheme={true}
/>
</div>
),
{
info: {
text: 'The dark theme is automatically used when dark mode is enabled in Kibana',
},
}
)
.add(
'transparent background',
() => (
<div>
<CodeEditor
languageId="plaintext"
height={250}
value="Hello!"
onChange={action('onChange')}
transparentBackground
/>
</div>
),
{
info: {
text: 'Plaintext Monaco Editor',
},
}
)
.add(
'custom log language',
() => (
<div>
<CodeEditor languageId="loglang" height={250} value={logs} onChange={action('onChange')} />
</div>
),
{
info: {
text: 'Custom language example. Language definition taken from [here](https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages)',
},
}
)
.add(
'hide minimap',
() => (
<div>
<CodeEditor
languageId="loglang"
height={250}
value={logs}
onChange={action('onChange')}
options={{
minimap: {
enabled: false,
},
}}
/>
</div>
),
{
info: {
text: 'The minimap (on left side of editor) can be disabled to save space',
},
}
)
.add(
'suggestion provider',
() => {
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>
);
},
{
info: {
text: 'Example suggestion provider is triggered by the `.` character',
},
}
)
.add(
'hover provider',
() => {
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>
);
},
{
info: {
text: 'Hover dialog example can be triggered by hovering over a word',
},
}
)
.add(
'json support',
() => (
<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>
),
{
info: { text: 'JSON language support' },
}
);

View file

@ -1,120 +0,0 @@
/*
* 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);
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

@ -1,233 +0,0 @@
/*
* 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';
// This import needs to come before './code_editor' below as it sets the jest.mocks
import { mockedEditorInstance } from './code_editor.test.helpers';
import { CodeEditor } from './code_editor';
// disabled because this is a test, but also it seems we shouldn't need this?
/* eslint-disable-next-line @kbn/eslint/module_migration */
import 'monaco-editor/esm/vs/basic-languages/html/html.contribution.js';
// 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 and then redefined as new values come through from the theme$ observable
expect((monaco.editor.defineTheme as jest.Mock).mock.calls.length).toBe(4);
// 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(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().props() as any).className).not.toContain('isInactive');
getHint().simulate('keydown', { key: keys.ENTER });
expect((getHint().props() as any).className).toContain('isInactive');
});
test('should be enabled when hitting the ESC key', () => {
getHint().simulate('keydown', { key: keys.ENTER });
expect((getHint().props() as any).className).toContain('isInactive');
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;
const getPlaceholderDomElement = (): HTMLElement | null =>
component.getDOMNode().querySelector('.kibanaCodeEditor__placeholderContainer');
beforeEach(() => {
component = mountWithIntl(
<CodeEditor
languageId="loglang"
height={250}
value=""
onChange={() => {}}
placeholder="myplaceholder"
/>
);
});
it('displays placeholder element when placeholder text is provided', () => {
expect(getPlaceholderDomElement()?.innerText).toBe('myplaceholder');
});
it('does not display placeholder element when placeholder text is not provided', () => {
component.setProps({ ...component.props(), placeholder: undefined, value: '' });
component.update();
expect(getPlaceholderDomElement()).toBe(null);
});
it('does not display placeholder element when user input has been provided', () => {
component.setProps({ ...component.props(), value: 'some input' });
component.update();
expect(getPlaceholderDomElement()).toBe(null);
});
});
});

View file

@ -6,587 +6,10 @@
* 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 classNames from 'classnames';
import './register_languages';
import { remeasureFonts } from './remeasure_fonts';
export type { CodeEditorProps as Props } from '@kbn/code-editor';
import {
DARK_THEME,
LIGHT_THEME,
DARK_THEME_TRANSPARENT,
LIGHT_THEME_TRANSPARENT,
} from './editor_theme';
import { PlaceholderWidget } from './placeholder_widget';
import './editor.scss';
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: 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,
height,
options,
overrideEditorWillMount,
editorDidMount,
editorWillMount,
useDarkTheme: useDarkThemeProp,
transparentBackground,
suggestionProvider,
signatureProvider,
hoverProvider,
placeholder,
languageConfiguration,
'aria-label': ariaLabel = i18n.translate('kibana-react.kibanaCodeEditor.ariaLabel', {
defaultMessage: 'Code Editor',
}),
isCopyable = false,
allowFullScreen = false,
}) => {
const { colorMode } = useEuiTheme();
const useDarkTheme = useDarkThemeProp ?? colorMode === 'DARK';
// 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 promptClasses = classNames('kibanaCodeEditor__keyboardHint', {
'kibanaCodeEditor__keyboardHint--isInactive': !isHintActive,
});
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]
);
const onBlurMonaco = useCallback(() => {
stopEditing();
}, [stopEditing]);
const renderPrompt = useCallback(() => {
const enterKey = (
<strong>
{i18n.translate('kibana-react.kibanaCodeEditor.enterKeyLabel', {
defaultMessage: 'Enter',
description:
'The name used for the Enter key on keyword. Will be {key} in kibana-react.kibanaCodeEditor.startEditing(ReadOnly).',
})}
</strong>
);
const escapeKey = (
<strong>
{i18n.translate('kibana-react.kibanaCodeEditor.escapeKeyLabel', {
defaultMessage: 'Esc',
description:
'The label of the Escape key as printed on the keyboard. Will be {key} inside kibana-react.kibanaCodeEditor.stopEditing(ReadOnly).',
})}
</strong>
);
return (
<EuiToolTip
display="block"
content={
<>
<p>
{isReadOnly ? (
<FormattedMessage
id="kibana-react.kibanaCodeEditor.startEditingReadOnly"
defaultMessage="Press {key} to start interacting with the code."
values={{ key: enterKey }}
/>
) : (
<FormattedMessage
id="kibana-react.kibanaCodeEditor.startEditing"
defaultMessage="Press {key} to start editing."
values={{ key: enterKey }}
/>
)}
</p>
<p>
{isReadOnly ? (
<FormattedMessage
id="kibana-react.kibanaCodeEditor.stopEditingReadOnly"
defaultMessage="Press {key} to stop interacting with the code."
values={{ key: escapeKey }}
/>
) : (
<FormattedMessage
id="kibana-react.kibanaCodeEditor.stopEditing"
defaultMessage="Press {key} to stop editing."
values={{ key: escapeKey }}
/>
)}
</p>
</>
}
>
<div
className={promptClasses}
id={htmlIdGenerator('codeEditor')()}
ref={editorHint}
tabIndex={0}
role="button"
onClick={startEditing}
onKeyDown={onKeyDownHint}
aria-label={ariaLabel}
data-test-subj="codeEditorHint"
/>
</EuiToolTip>
);
}, [onKeyDownHint, promptClasses, startEditing]);
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);
}
});
// Register themes
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,
]
);
useEffect(() => {
// Register themes when 'useDarkThem' changes
monaco.editor.defineTheme('euiColors', useDarkTheme ? DARK_THEME : LIGHT_THEME);
monaco.editor.defineTheme(
'euiColorsTransparent',
useDarkTheme ? DARK_THEME_TRANSPARENT : LIGHT_THEME_TRANSPARENT
);
}, [useDarkTheme]);
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]
);
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 });
return (
<div className="kibanaCodeEditor" onKeyDown={onKeyDown} data-test-subj="kibanaCodeEditor">
{renderPrompt()}
<FullScreenDisplay>
{allowFullScreen || isCopyable ? (
<div className="kibanaCodeEditor__controls">
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<CopyButton />
</EuiFlexItem>
<EuiFlexItem>
<FullScreenButton />
</EuiFlexItem>
</EuiFlexGroup>
</div>
) : null}
<MonacoEditor
theme={options?.theme ?? (transparentBackground ? 'euiColorsTransparent' : 'euiColors')}
language={languageId}
value={value}
onChange={onChange}
width={isFullScreen ? '100vw' : width}
height={isFullScreen ? '100vh' : height}
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
className="euiCodeBlock__fullScreenButton"
onClick={toggleFullScreen}
iconType={isFullScreen ? 'fullScreenExit' : 'fullScreen'}
color="text"
aria-label={isFullScreen ? fullscreenCollapse : fullscreenExpand}
size="xs"
/>
)}
</EuiI18n>
);
};
const FullScreenDisplay = useMemo(
() =>
({ children }: { children: Array<JSX.Element | null> | JSX.Element }) => {
if (!isFullScreen) return <>{children}</>;
return (
<EuiOverlayMask>
<EuiFocusTrap clickOutsideDisables={true}>
<div className={'kibanaCodeEditor__isFullScreen'}>{children}</div>
</EuiFocusTrap>
</EuiOverlayMask>
);
},
[isFullScreen]
);
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 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 };
};
import { CodeEditor } from '@kbn/code-editor';
export { CodeEditor };
// React.lazy requires default export
// eslint-disable-next-line import/no-default-export

View file

@ -1,47 +0,0 @@
.react-monaco-editor-container .monaco-editor .inputarea:focus {
animation: none !important; // Removes textarea EUI blue underline animation from EUI
}
.kibanaCodeEditor {
position: relative;
height: 100%;
&__placeholderContainer {
color: $euiTextSubduedColor;
width: max-content;
pointer-events: none;
}
&__keyboardHint {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
&:focus {
z-index: $euiZLevel1;
}
&--isInactive {
display: none;
}
}
&__controls {
top: $euiSizeXS;
right: $euiSize;
position: absolute;
z-index: 1000
}
&__isFullScreen {
position: absolute;
left: 0;
top: 0;
.kibanaCodeEditor__controls {
top: $euiSizeL;
right: $euiSizeL;
}
}
}

View file

@ -11,7 +11,7 @@ import { EuiDelayRender, EuiErrorBoundary, EuiSkeletonText, useEuiTheme } from '
import type { Props } from './code_editor';
export * from './languages/constants';
export * from '@kbn/code-editor/languages/constants';
const LazyBaseEditor = React.lazy(() => import('./code_editor'));
const LazyCodeEditorField = React.lazy(() =>

View file

@ -1,198 +0,0 @@
/*
* 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

@ -1,90 +0,0 @@
/*
* 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

@ -1,49 +0,0 @@
/*
* 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

@ -1,28 +0,0 @@
/*
* 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 function 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

@ -23,6 +23,7 @@
"@kbn/core-theme-browser",
"@kbn/react-kibana-context-common",
"@kbn/react-kibana-context-styled",
"@kbn/code-editor",
],
"exclude": [
"target/**/*",

View file

@ -150,12 +150,8 @@
"@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/code-editor": ["packages/shared-ux/code_editor"],
"@kbn/code-editor/*": ["packages/shared-ux/code_editor/*"],
"@kbn/coloring": ["packages/kbn-coloring"],
"@kbn/coloring/*": ["packages/kbn-coloring/*"],
"@kbn/config": ["packages/kbn-config"],

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import '@kbn/kibana-react-plugin/public/code_editor/code_editor.test.helpers';
import '@kbn/code-editor/code_editor.test.helpers';
import { TestProvider } from '../../test/test_provider';
import {
getCloudDefendNewPolicyMock,

View file

@ -17,7 +17,7 @@ import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
import type { CloudDefendPluginStartDeps } from '../types';
import './__mocks__/worker';
import './__mocks__/resizeobserver';
import '@kbn/kibana-react-plugin/public/code_editor/code_editor.test.helpers';
import '@kbn/code-editor/code_editor.test.helpers';
// @ts-ignore-next
window.Worker = Worker;

View file

@ -35,7 +35,8 @@
"@kbn/utility-types-jest",
"@kbn/kubernetes-security-plugin",
"@kbn/core-http-router-server-mocks",
"@kbn/core-elasticsearch-server"
"@kbn/core-elasticsearch-server",
"@kbn/code-editor"
],
"exclude": ["target/**/*"]
}

View file

@ -4716,10 +4716,6 @@
"kibana_utils.stateManagement.url.restoreUrlErrorTitle": "Erreur lors de la restauration de l'état depuis l'URL.",
"kibana_utils.stateManagement.url.saveStateInUrlErrorTitle": "Erreur lors de l'enregistrement de l'état dans l'URL.",
"kibana-react.dualRangeControl.outsideOfRangeErrorMessage": "Les valeurs doivent être comprises entre {min} et {max}, inclus",
"kibana-react.kibanaCodeEditor.startEditing": "Appuyez sur {key} pour démarrer la modification.",
"kibana-react.kibanaCodeEditor.startEditingReadOnly": "Appuyez sur {key} pour commencer à interagir avec le code.",
"kibana-react.kibanaCodeEditor.stopEditing": "Appuyez sur {key} pour arrêter la modification.",
"kibana-react.kibanaCodeEditor.stopEditingReadOnly": "Appuyez sur {key} pour arrêter d'interagir avec le code.",
"kibana-react.noDataPage.cantDecide": "Vous ne savez pas quoi utiliser ? {link}",
"kibana-react.noDataPage.intro": "Ajoutez vos données pour commencer, ou {link} sur {solution}.",
"kibana-react.noDataPage.welcomeTitle": "Bienvenue dans Elastic {solution} !",
@ -4731,9 +4727,6 @@
"kibana-react.kbnOverviewPageHeader.addIntegrationsButtonLabel": "Ajouter des intégrations",
"kibana-react.kbnOverviewPageHeader.devToolsButtonLabel": "Outils de développement",
"kibana-react.kbnOverviewPageHeader.stackManagementButtonLabel": "Gérer",
"kibana-react.kibanaCodeEditor.ariaLabel": "Éditeur de code",
"kibana-react.kibanaCodeEditor.enterKeyLabel": "Entrée",
"kibana-react.kibanaCodeEditor.escapeKeyLabel": "Échap",
"kibana-react.noDataPage.cantDecide.link": "Consultez la documentation pour en savoir plus.",
"kibana-react.noDataPage.elasticAgentCard.description": "Utilisez Elastic Agent pour collecter de manière simple et unifiée les données de vos machines.",
"kibana-react.noDataPage.elasticAgentCard.noPermission.description": "Cette intégration n'est pas encore activée. Votre administrateur possède les autorisations requises pour l'activer.",

View file

@ -4732,10 +4732,6 @@
"kibana_utils.stateManagement.url.restoreUrlErrorTitle": "URLからの状態の復元エラー",
"kibana_utils.stateManagement.url.saveStateInUrlErrorTitle": "URLでの状態の保存エラー",
"kibana-react.dualRangeControl.outsideOfRangeErrorMessage": "値は{min}と{max}の間でなければなりません",
"kibana-react.kibanaCodeEditor.startEditing": "編集を開始するには{key}を押してください。",
"kibana-react.kibanaCodeEditor.startEditingReadOnly": "コードの操作を開始するには{key}を押してください。",
"kibana-react.kibanaCodeEditor.stopEditing": "編集を停止するには{key}を押してください。",
"kibana-react.kibanaCodeEditor.stopEditingReadOnly": "コードの操作を停止するには{key}を押してください。",
"kibana-react.noDataPage.cantDecide": "どれを使用すべきかわからない場合は{link}",
"kibana-react.noDataPage.intro": "データを追加して開始するか、{solution}については{link}をご覧ください。",
"kibana-react.noDataPage.welcomeTitle": "Elastic {solution}へようこそ!",
@ -4747,9 +4743,6 @@
"kibana-react.kbnOverviewPageHeader.addIntegrationsButtonLabel": "統合の追加",
"kibana-react.kbnOverviewPageHeader.devToolsButtonLabel": "開発ツール",
"kibana-react.kbnOverviewPageHeader.stackManagementButtonLabel": "管理",
"kibana-react.kibanaCodeEditor.ariaLabel": "コードエディター",
"kibana-react.kibanaCodeEditor.enterKeyLabel": "Enter",
"kibana-react.kibanaCodeEditor.escapeKeyLabel": "Esc",
"kibana-react.noDataPage.cantDecide.link": "詳細については、ドキュメントをご確認ください。",
"kibana-react.noDataPage.elasticAgentCard.description": "Elasticエージェントを使用すると、シンプルで統一された方法でコンピューターからデータを収集するできます。",
"kibana-react.noDataPage.elasticAgentCard.noPermission.description": "この統合はまだ有効ではありません。管理者にはオンにするために必要なアクセス権があります。",

View file

@ -4731,10 +4731,6 @@
"kibana_utils.stateManagement.url.restoreUrlErrorTitle": "从 URL 还原状态时出错",
"kibana_utils.stateManagement.url.saveStateInUrlErrorTitle": "在 URL 中保存状态时出错",
"kibana-react.dualRangeControl.outsideOfRangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内",
"kibana-react.kibanaCodeEditor.startEditing": "按 {key} 键开始编辑。",
"kibana-react.kibanaCodeEditor.startEditingReadOnly": "按 {key} 键开始与代码互动。",
"kibana-react.kibanaCodeEditor.stopEditing": "按 {key} 键停止编辑。",
"kibana-react.kibanaCodeEditor.stopEditingReadOnly": "按 {key} 键停止与代码互动。",
"kibana-react.noDataPage.cantDecide": "不知道使用哪一个?{link}",
"kibana-react.noDataPage.intro": "添加您的数据以开始,或{link}{solution}。",
"kibana-react.noDataPage.welcomeTitle": "欢迎使用 Elastic {solution}",
@ -4746,9 +4742,6 @@
"kibana-react.kbnOverviewPageHeader.addIntegrationsButtonLabel": "添加集成",
"kibana-react.kbnOverviewPageHeader.devToolsButtonLabel": "开发工具",
"kibana-react.kbnOverviewPageHeader.stackManagementButtonLabel": "管理",
"kibana-react.kibanaCodeEditor.ariaLabel": "代码编辑器",
"kibana-react.kibanaCodeEditor.enterKeyLabel": "Enter",
"kibana-react.kibanaCodeEditor.escapeKeyLabel": "Esc",
"kibana-react.noDataPage.cantDecide.link": "请参阅我们的文档以了解更多信息。",
"kibana-react.noDataPage.elasticAgentCard.description": "使用 Elastic 代理以简单统一的方式从您的计算机中收集数据。",
"kibana-react.noDataPage.elasticAgentCard.noPermission.description": "尚未启用此集成。您的管理员具有打开它所需的权限。",

View file

@ -30,7 +30,7 @@ export function MachineLearningJobWizardAdvancedProvider(
},
async assertDatafeedQueryEditorExists() {
await testSubjects.existOrFail('mlAdvancedDatafeedQueryEditor > codeEditorHint');
await testSubjects.existOrFail('mlAdvancedDatafeedQueryEditor > ~codeEditorHint');
},
async assertDatafeedQueryEditorValue(expectedValue: string) {

View file

@ -3204,15 +3204,7 @@
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":
"@kbn/code-editor@link:packages/shared-ux/code_editor":
version "0.0.0"
uid ""