mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* First version of Editor component and integration with the expression editor * Adding resize detector * Remove blue border on editor select * Adding types for the react resize detector * Adding worker and a few more monaco plugins * Suggestion completion rework * Add resize detector types as well as an IE11 full width bug fix * Adding correct types for function definitions and monaco * change CSS class names, add border to input * Adding boolean styling * Slight refactor of canvas function/arg types and adding first pass of hover * Fixing hover interaction for functions and arguments * Namespacing Code monaco css overrides * Styling cleanup and simple README * Setting up tests including some storyshots for the ExpressionInput component and Editor component * Prop documentation for both the ExpressionInput and Editor components * Adding Editor snapshots * tiny cleanup * Moving language registration, adding autocomplete suggestion types, and cleaning up editor * Some documentation and cleanup from PR feedback * Fixing types, adding documentation * clean up editor, remove autocomplete toggle * More PR cleanup * Test fix, type fix * fix issues around errors. code cleanup
This commit is contained in:
parent
0e85f914c5
commit
6175c3534f
37 changed files with 1198 additions and 566 deletions
|
@ -44,8 +44,9 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) {
|
|||
'^.+\\.html?$': 'jest-raw-loader',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
// ignore all node_modules except @elastic/eui which requires babel transforms to handle dynamic import()
|
||||
'[/\\\\]node_modules(?![\\/\\\\]@elastic[\\/\\\\]eui)[/\\\\].+\\.js$',
|
||||
// ignore all node_modules except @elastic/eui and monaco-editor which both require babel transforms to handle dynamic import()
|
||||
// since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842)
|
||||
'[/\\\\]node_modules(?![\\/\\\\]@elastic[\\/\\\\]eui)(?![\\/\\\\]monaco-editor)[/\\\\].+\\.js$',
|
||||
],
|
||||
snapshotSerializers: [`${kibanaDirectory}/node_modules/enzyme-to-json/serializer`],
|
||||
reporters: [
|
||||
|
|
|
@ -43,6 +43,8 @@ jest.mock('@elastic/eui/packages/react-datepicker', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('plugins/interpreter/registries', () => ({}));
|
||||
|
||||
// Disabling this test due to https://github.com/elastic/eui/issues/2242
|
||||
jest.mock(
|
||||
'../public/components/workpad_header/workpad_export/__examples__/disabled_panel.examples',
|
||||
|
|
|
@ -127,6 +127,10 @@ module.exports = async ({ config }) => {
|
|||
KIBANA_ROOT,
|
||||
'packages/kbn-interpreter/target/common'
|
||||
);
|
||||
config.resolve.alias['plugins/interpreter/registries'] = path.resolve(
|
||||
KIBANA_ROOT,
|
||||
'packages/kbn-interpreter/target/common/registries'
|
||||
);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ const TextAreaArgInput = ({ updateValue, value, confirm, commit, renderError, ar
|
|||
return (
|
||||
<EuiForm>
|
||||
<EuiTextArea
|
||||
className="canvasTextArea--code"
|
||||
className="canvasTextArea__code"
|
||||
id={argId}
|
||||
rows={10}
|
||||
value={value}
|
||||
|
|
|
@ -60,7 +60,7 @@ class EssqlDatasource extends PureComponent {
|
|||
<EuiTextArea
|
||||
placeholder={this.defaultQuery}
|
||||
isInvalid={isInvalid}
|
||||
className="canvasTextArea--code"
|
||||
className="canvasTextArea__code"
|
||||
value={this.getQuery()}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
|
|
|
@ -65,7 +65,7 @@ const TimelionDatasource = ({ args, updateArgs, defaultIndex }) => {
|
|||
|
||||
<EuiFormRow label="Query" helpText="Lucene Query String syntax">
|
||||
<EuiTextArea
|
||||
className="canvasTextArea--code"
|
||||
className="canvasTextArea__code"
|
||||
value={getQuery()}
|
||||
onChange={e => setArg(argName, e.target.value)}
|
||||
/>
|
||||
|
|
|
@ -12,10 +12,48 @@ import {
|
|||
ExpressionFunctionAST,
|
||||
ExpressionArgAST,
|
||||
CanvasFunction,
|
||||
CanvasArg,
|
||||
CanvasArgValue,
|
||||
} from '../../types';
|
||||
|
||||
const MARKER = 'CANVAS_SUGGESTION_MARKER';
|
||||
|
||||
interface BaseSuggestion {
|
||||
text: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
interface FunctionSuggestion extends BaseSuggestion {
|
||||
type: 'function';
|
||||
fnDef: CanvasFunction;
|
||||
}
|
||||
|
||||
type ArgSuggestionValue = CanvasArgValue & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
interface ArgSuggestion extends BaseSuggestion {
|
||||
type: 'argument';
|
||||
argDef: ArgSuggestionValue;
|
||||
}
|
||||
|
||||
interface ValueSuggestion extends BaseSuggestion {
|
||||
type: 'value';
|
||||
}
|
||||
|
||||
export type AutocompleteSuggestion = FunctionSuggestion | ArgSuggestion | ValueSuggestion;
|
||||
|
||||
interface FnArgAtPosition {
|
||||
ast: ExpressionASTWithMeta;
|
||||
fnIndex: number;
|
||||
|
||||
argName?: string;
|
||||
argIndex?: number;
|
||||
argStart?: number;
|
||||
argEnd?: number;
|
||||
}
|
||||
|
||||
// If you parse an expression with the "addMeta" option it completely
|
||||
// changes the type of returned object. The following types
|
||||
// enhance the existing AST types with the appropriate meta information
|
||||
|
@ -62,19 +100,14 @@ function isExpression(
|
|||
return typeof maybeExpression.node === 'object';
|
||||
}
|
||||
|
||||
type valueof<T> = T[keyof T];
|
||||
type ValuesOfUnion<T> = T extends any ? valueof<T> : never;
|
||||
|
||||
// All of the possible Arg Values
|
||||
type ArgValue = ValuesOfUnion<CanvasFunction['args']>;
|
||||
// All of the argument objects
|
||||
type CanvasArg = CanvasFunction['args'];
|
||||
|
||||
// Overloads to change return type based on specs
|
||||
function getByAlias(specs: CanvasFunction[], name: string): CanvasFunction;
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
function getByAlias(specs: CanvasArg, name: string): ArgValue;
|
||||
function getByAlias(specs: CanvasFunction[] | CanvasArg, name: string): CanvasFunction | ArgValue {
|
||||
function getByAlias(specs: CanvasArg, name: string): CanvasArgValue;
|
||||
function getByAlias(
|
||||
specs: CanvasFunction[] | CanvasArg,
|
||||
name: string
|
||||
): CanvasFunction | CanvasArgValue {
|
||||
return untypedGetByAlias(specs, name);
|
||||
}
|
||||
|
||||
|
@ -87,23 +120,24 @@ export function getFnArgDefAtPosition(
|
|||
expression: string,
|
||||
position: number
|
||||
) {
|
||||
const text = expression.substr(0, position) + MARKER + expression.substr(position);
|
||||
try {
|
||||
const ast: ExpressionASTWithMeta = parse(text, { addMeta: true }) as ExpressionASTWithMeta;
|
||||
const ast: ExpressionASTWithMeta = parse(expression, {
|
||||
addMeta: true,
|
||||
}) as ExpressionASTWithMeta;
|
||||
|
||||
const { ast: newAst, fnIndex, argName } = getFnArgAtPosition(ast, position);
|
||||
const { ast: newAst, fnIndex, argName, argStart, argEnd } = getFnArgAtPosition(ast, position);
|
||||
const fn = newAst.node.chain[fnIndex].node;
|
||||
|
||||
const fnDef = getByAlias(specs, fn.function.replace(MARKER, ''));
|
||||
const fnDef = getByAlias(specs, fn.function);
|
||||
if (fnDef && argName) {
|
||||
const argDef = getByAlias(fnDef.args, argName);
|
||||
return { fnDef, argDef };
|
||||
return { fnDef, argDef, argStart, argEnd };
|
||||
}
|
||||
return { fnDef };
|
||||
} catch (e) {
|
||||
// Fail silently
|
||||
}
|
||||
return [];
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,7 +151,7 @@ export function getAutocompleteSuggestions(
|
|||
specs: CanvasFunction[],
|
||||
expression: string,
|
||||
position: number
|
||||
) {
|
||||
): AutocompleteSuggestion[] {
|
||||
const text = expression.substr(0, position) + MARKER + expression.substr(position);
|
||||
try {
|
||||
const ast = parse(text, { addMeta: true }) as ExpressionASTWithMeta;
|
||||
|
@ -151,20 +185,39 @@ export function getAutocompleteSuggestions(
|
|||
It returns which function the cursor is in, as well as which argument for that function the cursor is in
|
||||
if any.
|
||||
*/
|
||||
function getFnArgAtPosition(
|
||||
ast: ExpressionASTWithMeta,
|
||||
position: number
|
||||
): { ast: ExpressionASTWithMeta; fnIndex: number; argName?: string; argIndex?: number } {
|
||||
function getFnArgAtPosition(ast: ExpressionASTWithMeta, position: number): FnArgAtPosition {
|
||||
const fnIndex = ast.node.chain.findIndex(fn => fn.start <= position && position <= fn.end);
|
||||
const fn = ast.node.chain[fnIndex];
|
||||
for (const [argName, argValues] of Object.entries(fn.node.arguments)) {
|
||||
for (let argIndex = 0; argIndex < argValues.length; argIndex++) {
|
||||
const value = argValues[argIndex];
|
||||
if (value.start <= position && position <= value.end) {
|
||||
|
||||
let argStart = value.start;
|
||||
let argEnd = value.end;
|
||||
if (argName !== '_') {
|
||||
// If an arg name is specified, expand our start position to include
|
||||
// the arg name plus the `=` character
|
||||
argStart = argStart - (argName.length + 1);
|
||||
|
||||
// If the arg value is an expression, expand our start and end position
|
||||
// to include the opening and closing braces
|
||||
if (value.node !== null && isExpression(value)) {
|
||||
argStart--;
|
||||
argEnd++;
|
||||
}
|
||||
}
|
||||
|
||||
if (argStart <= position && position <= argEnd) {
|
||||
// If the current position is on an expression and NOT on the expression's
|
||||
// argument name (`font=` for example), recurse within the expression
|
||||
if (
|
||||
value.node !== null &&
|
||||
isExpression(value) &&
|
||||
(argName === '_' || !(argStart <= position && position <= argStart + argName.length + 1))
|
||||
) {
|
||||
return getFnArgAtPosition(value, position);
|
||||
}
|
||||
return { ast, fnIndex, argName, argIndex };
|
||||
return { ast, fnIndex, argName, argIndex, argStart, argEnd };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -175,7 +228,7 @@ function getFnNameSuggestions(
|
|||
specs: CanvasFunction[],
|
||||
ast: ExpressionASTWithMeta,
|
||||
fnIndex: number
|
||||
) {
|
||||
): FunctionSuggestion[] {
|
||||
// Filter the list of functions by the text at the marker
|
||||
const { start, end, node: fn } = ast.node.chain[fnIndex];
|
||||
const query = fn.function.replace(MARKER, '');
|
||||
|
@ -205,7 +258,7 @@ function getArgNameSuggestions(
|
|||
fnIndex: number,
|
||||
argName: string,
|
||||
argIndex: number
|
||||
) {
|
||||
): ArgSuggestion[] {
|
||||
// Get the list of args from the function definition
|
||||
const fn = ast.node.chain[fnIndex].node;
|
||||
const fnDef = getByAlias(specs, fn.function);
|
||||
|
@ -218,7 +271,7 @@ function getArgNameSuggestions(
|
|||
|
||||
// Filter the list of args by the text at the marker
|
||||
const query = text.replace(MARKER, '');
|
||||
const matchingArgDefs = Object.entries<ArgValue>(fnDef.args).filter(([name]) =>
|
||||
const matchingArgDefs = Object.entries<CanvasArgValue>(fnDef.args).filter(([name]) =>
|
||||
textMatches(name, query)
|
||||
);
|
||||
|
||||
|
@ -245,11 +298,11 @@ function getArgNameSuggestions(
|
|||
// with the text at the marker, then alphabetically
|
||||
const comparator = combinedComparator(
|
||||
unnamedArgComparator,
|
||||
invokeWithProp<string, 'name', ArgValue & { name: string }, number>(
|
||||
invokeWithProp<string, 'name', CanvasArgValue & { name: string }, number>(
|
||||
startsWithComparator(query),
|
||||
'name'
|
||||
),
|
||||
invokeWithProp<string, 'name', ArgValue & { name: string }, number>(
|
||||
invokeWithProp<string, 'name', CanvasArgValue & { name: string }, number>(
|
||||
alphanumericalComparator,
|
||||
'name'
|
||||
)
|
||||
|
@ -267,7 +320,7 @@ function getArgValueSuggestions(
|
|||
fnIndex: number,
|
||||
argName: string,
|
||||
argIndex: number
|
||||
) {
|
||||
): ValueSuggestion[] {
|
||||
// Get the list of values from the argument definition
|
||||
const fn = ast.node.chain[fnIndex].node;
|
||||
const fnDef = getByAlias(specs, fn.function);
|
||||
|
@ -331,7 +384,7 @@ function prevFnTypeComparator(prevFnType: any) {
|
|||
};
|
||||
}
|
||||
|
||||
function unnamedArgComparator(a: ArgValue, b: ArgValue): number {
|
||||
function unnamedArgComparator(a: CanvasArgValue, b: CanvasArgValue): number {
|
||||
return (
|
||||
(b.aliases && b.aliases.includes('_') ? 1 : 0) - (a.aliases && a.aliases.includes('_') ? 1 : 0)
|
||||
);
|
||||
|
|
|
@ -16,8 +16,6 @@ export const API_ROUTE_WORKPAD_STRUCTURES = `${API_ROUTE}/workpad-structures`;
|
|||
export const API_ROUTE_CUSTOM_ELEMENT = `${API_ROUTE}/custom-element`;
|
||||
export const LOCALSTORAGE_PREFIX = `kibana.canvas`;
|
||||
export const LOCALSTORAGE_CLIPBOARD = `${LOCALSTORAGE_PREFIX}.clipboard`;
|
||||
export const LOCALSTORAGE_AUTOCOMPLETE_ENABLED = `${LOCALSTORAGE_PREFIX}.isAutocompleteEnabled`;
|
||||
export const LOCALSTORAGE_EXPRESSION_EDITOR_FONT_SIZE = `${LOCALSTORAGE_PREFIX}.expressionEditorFontSize`;
|
||||
export const LOCALSTORAGE_LASTPAGE = 'canvas:lastpage';
|
||||
export const FETCH_TIMEOUT = 30000; // 30 seconds
|
||||
export const CANVAS_USAGE_TYPE = 'canvas';
|
||||
|
|
|
@ -12,6 +12,7 @@ $canvasLayoutFontSize: $euiFontSizeS;
|
|||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
max-height: 100vh;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.canvasLayout__cols {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { getInterpreter } from 'plugins/interpreter/interpreter';
|
|||
import { getAppReady, getBasePath } from '../../state/selectors/app';
|
||||
import { appReady, appError } from '../../state/actions/app';
|
||||
import { elementsRegistry } from '../../lib/elements_registry';
|
||||
import { registerLanguage } from '../../lib/monaco_language_def';
|
||||
import { templatesRegistry } from '../../lib/templates_registry';
|
||||
import { tagsRegistry } from '../../lib/tags_registry';
|
||||
import { elementSpecs } from '../../../canvas_plugin_src/elements';
|
||||
|
@ -72,6 +73,9 @@ const mapDispatchToProps = dispatch => ({
|
|||
try {
|
||||
await getInterpreter();
|
||||
|
||||
// Register the expression language with the Monaco Editor
|
||||
registerLanguage();
|
||||
|
||||
// set app state to ready
|
||||
dispatch(appReady());
|
||||
} catch (e) {
|
||||
|
|
|
@ -254,7 +254,7 @@ export class Autocomplete extends React.Component {
|
|||
) : (
|
||||
''
|
||||
)}
|
||||
<div className="canvasAutocomplete--inner" onMouseDown={this.onMouseDown}>
|
||||
<div className="canvasAutocomplete__inner" onMouseDown={this.onMouseDown}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# Editor Component
|
||||
|
||||
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)
|
||||
|
||||
[_TODO: Examples of each_](https://github.com/elastic/kibana/issues/43812)
|
||||
|
||||
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
|
|
@ -0,0 +1,46 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Storyshots components/Editor custom log language 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="react-monaco-editor-container"
|
||||
style={
|
||||
Object {
|
||||
"height": "250px",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div />
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots components/Editor default 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="react-monaco-editor-container"
|
||||
style={
|
||||
Object {
|
||||
"height": "250px",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div />
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots components/Editor html 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="react-monaco-editor-container"
|
||||
style={
|
||||
Object {
|
||||
"height": "250px",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div />
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { Editor } from '../editor';
|
||||
|
||||
import 'monaco-editor/esm/vs/basic-languages/html/html.contribution.js';
|
||||
|
||||
// A sample language definition with a few example tokens
|
||||
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
|
||||
`;
|
||||
|
||||
const html = `<section>
|
||||
<span>Hello World!</span>
|
||||
</section>`;
|
||||
|
||||
storiesOf('components/Editor', module)
|
||||
.add('default', () => (
|
||||
<div>
|
||||
<Editor languageId="plaintext" height={250} value="Hello!" onChange={action('onChange')} />
|
||||
</div>
|
||||
))
|
||||
.add('html', () => (
|
||||
<div>
|
||||
<Editor languageId="html" height={250} value={html} onChange={action('onChange')} />
|
||||
</div>
|
||||
))
|
||||
.add('custom log language', () => (
|
||||
<div>
|
||||
<Editor languageId="loglang" height={250} value={logs} onChange={action('onChange')} />
|
||||
</div>
|
||||
));
|
|
@ -0,0 +1,3 @@
|
|||
.react-monaco-editor-container .monaco-editor .inputarea:focus {
|
||||
animation: none; // Removes textarea EUI blue underline animation from EUI
|
||||
}
|
158
x-pack/legacy/plugins/canvas/public/components/editor/editor.tsx
Normal file
158
x-pack/legacy/plugins/canvas/public/components/editor/editor.tsx
Normal file
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactResizeDetector from 'react-resize-detector';
|
||||
import MonacoEditor, { EditorDidMount, EditorWillMount } from 'react-monaco-editor';
|
||||
|
||||
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import 'monaco-editor/esm/vs/base/common/worker/simpleWorker';
|
||||
import 'monaco-editor/esm/vs/base/worker/defaultWorkerFactory';
|
||||
|
||||
import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands.js';
|
||||
import 'monaco-editor/esm/vs/editor/browser/widget/codeEditorWidget.js';
|
||||
|
||||
import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController.js'; // Needed for suggestions
|
||||
import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover
|
||||
import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature
|
||||
|
||||
import { theme } from './editor_theme';
|
||||
|
||||
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) => void;
|
||||
|
||||
/**
|
||||
* Options for the Monaco Code Editor
|
||||
* Documentation of options can be found here:
|
||||
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.ieditorconstructionoptions.html
|
||||
*/
|
||||
options?: monacoEditor.editor.IEditorConstructionOptions;
|
||||
|
||||
/**
|
||||
* 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?: monacoEditor.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?: monacoEditor.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?: monacoEditor.languages.HoverProvider;
|
||||
|
||||
/**
|
||||
* Function called before the editor is mounted in the view
|
||||
*/
|
||||
editorWillMount?: EditorWillMount;
|
||||
/**
|
||||
* Function called before the editor is mounted in the view
|
||||
* and completely replaces the setup behavior called by the component
|
||||
*/
|
||||
overrideEditorWillMount?: EditorWillMount;
|
||||
|
||||
/**
|
||||
* Function called after the editor is mounted in the view
|
||||
*/
|
||||
editorDidMount?: EditorDidMount;
|
||||
}
|
||||
|
||||
export class Editor extends React.Component<Props, {}> {
|
||||
_editor: monacoEditor.editor.IStandaloneCodeEditor | null = null;
|
||||
|
||||
_editorWillMount = (monaco: typeof monacoEditor) => {
|
||||
if (this.props.overrideEditorWillMount) {
|
||||
this.props.overrideEditorWillMount(monaco);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.editorWillMount) {
|
||||
this.props.editorWillMount(monaco);
|
||||
}
|
||||
|
||||
monaco.languages.onLanguage(this.props.languageId, () => {
|
||||
if (this.props.suggestionProvider) {
|
||||
monaco.languages.registerCompletionItemProvider(
|
||||
this.props.languageId,
|
||||
this.props.suggestionProvider
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.signatureProvider) {
|
||||
monaco.languages.registerSignatureHelpProvider(
|
||||
this.props.languageId,
|
||||
this.props.signatureProvider
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.hoverProvider) {
|
||||
monaco.languages.registerHoverProvider(this.props.languageId, this.props.hoverProvider);
|
||||
}
|
||||
});
|
||||
|
||||
// Register the theme
|
||||
monaco.editor.defineTheme('euiColors', theme);
|
||||
};
|
||||
|
||||
_editorDidMount = (
|
||||
editor: monacoEditor.editor.IStandaloneCodeEditor,
|
||||
monaco: typeof monacoEditor
|
||||
) => {
|
||||
this._editor = editor;
|
||||
|
||||
if (this.props.editorDidMount) {
|
||||
this.props.editorDidMount(editor, monaco);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { languageId, value, onChange, width, height, options } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MonacoEditor
|
||||
theme="euiColors"
|
||||
language={languageId}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
editorWillMount={this._editorWillMount}
|
||||
editorDidMount={this._editorDidMount}
|
||||
width={width}
|
||||
height={height}
|
||||
options={options}
|
||||
/>
|
||||
<ReactResizeDetector handleWidth handleHeight onResize={this._updateDimensions} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
_updateDimensions = () => {
|
||||
if (this._editor) {
|
||||
this._editor.layout();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
|
||||
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
// NOTE: For talk around where this theme information will ultimately live,
|
||||
// please see this discuss issue: https://github.com/elastic/kibana/issues/43814
|
||||
|
||||
const IS_DARK_THEME = chrome.getUiSettingsClient().get('theme:darkMode');
|
||||
|
||||
const themeName = IS_DARK_THEME ? darkTheme : lightTheme;
|
||||
|
||||
const themeColors = {
|
||||
keyword: themeName.euiColorAccent,
|
||||
comment: themeName.euiColorDarkShade,
|
||||
delimiter: themeName.euiColorSecondary,
|
||||
string: themeName.euiColorPrimary,
|
||||
number: themeName.euiColorWarning,
|
||||
regexp: themeName.euiColorPrimary,
|
||||
types: `${IS_DARK_THEME ? themeName.euiColorVis5 : themeName.euiColorVis9}`,
|
||||
annotation: themeName.euiColorLightShade,
|
||||
tag: themeName.euiColorAccent,
|
||||
symbol: themeName.euiColorDanger,
|
||||
foreground: themeName.euiColorDarkestShade,
|
||||
editorBackground: themeName.euiColorEmptyShade,
|
||||
lineNumbers: themeName.euiColorDarkShade,
|
||||
editorIndentGuide: themeName.euiColorLightShade,
|
||||
selectionBackground: `${IS_DARK_THEME ? '#343551' : '#E3E4ED'}`,
|
||||
editorWidgetBackground: themeName.euiColorLightestShade,
|
||||
editorWidgetBorder: themeName.euiColorLightShade,
|
||||
findMatchBackground: themeName.euiColorWarning,
|
||||
findMatchHighlightBackground: themeName.euiColorWarning,
|
||||
};
|
||||
|
||||
export const theme: monacoEditor.editor.IStandaloneThemeData = {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{
|
||||
token: '',
|
||||
foreground: themeName.euiColorDarkestShade,
|
||||
background: themeName.euiColorEmptyShade,
|
||||
},
|
||||
{ token: 'invalid', foreground: themeName.euiColorAccent },
|
||||
{ token: 'emphasis', fontStyle: 'italic' },
|
||||
{ token: 'strong', fontStyle: 'bold' },
|
||||
|
||||
{ token: 'variable', foreground: themeName.euiColorPrimary },
|
||||
{ token: 'variable.predefined', foreground: themeName.euiColorSecondary },
|
||||
{ token: 'constant', foreground: themeName.euiColorAccent },
|
||||
{ token: 'comment', foreground: themeName.euiColorMediumShade },
|
||||
{ token: 'number', foreground: themeName.euiColorWarning },
|
||||
{ token: 'number.hex', foreground: themeName.euiColorPrimary },
|
||||
{ token: 'regexp', foreground: themeName.euiColorDanger },
|
||||
{ token: 'annotation', foreground: themeName.euiColorMediumShade },
|
||||
{ token: 'type', foreground: themeName.euiColorVis0 },
|
||||
|
||||
{ token: 'delimiter', foreground: themeName.euiColorDarkestShade },
|
||||
{ token: 'delimiter.html', foreground: themeName.euiColorDarkShade },
|
||||
{ token: 'delimiter.xml', foreground: themeName.euiColorPrimary },
|
||||
|
||||
{ token: 'tag', foreground: themeName.euiColorDanger },
|
||||
{ token: 'tag.id.jade', foreground: themeName.euiColorPrimary },
|
||||
{ token: 'tag.class.jade', foreground: themeName.euiColorPrimary },
|
||||
{ token: 'meta.scss', foreground: themeName.euiColorAccent },
|
||||
{ token: 'metatag', foreground: themeName.euiColorSecondary },
|
||||
{ token: 'metatag.content.html', foreground: themeName.euiColorDanger },
|
||||
{ token: 'metatag.html', foreground: themeName.euiColorMediumShade },
|
||||
{ token: 'metatag.xml', foreground: themeName.euiColorMediumShade },
|
||||
{ token: 'metatag.php', fontStyle: 'bold' },
|
||||
|
||||
{ token: 'key', foreground: themeName.euiColorWarning },
|
||||
{ token: 'string.key.json', foreground: themeName.euiColorDanger },
|
||||
{ token: 'string.value.json', foreground: themeName.euiColorPrimary },
|
||||
|
||||
{ token: 'attribute.name', foreground: themeName.euiColorDanger },
|
||||
{ token: 'attribute.name.css', foreground: themeName.euiColorSecondary },
|
||||
{ token: 'attribute.value', foreground: themeName.euiColorPrimary },
|
||||
{ token: 'attribute.value.number', foreground: themeName.euiColorWarning },
|
||||
{ token: 'attribute.value.unit', foreground: themeName.euiColorWarning },
|
||||
{ token: 'attribute.value.html', foreground: themeName.euiColorPrimary },
|
||||
{ token: 'attribute.value.xml', foreground: themeName.euiColorPrimary },
|
||||
|
||||
{ token: 'string', foreground: themeName.euiColorDanger },
|
||||
{ token: 'string.html', foreground: themeName.euiColorPrimary },
|
||||
{ token: 'string.sql', foreground: themeName.euiColorDanger },
|
||||
{ token: 'string.yaml', foreground: themeName.euiColorPrimary },
|
||||
|
||||
{ token: 'keyword', foreground: themeName.euiColorPrimary },
|
||||
{ token: 'keyword.json', foreground: themeName.euiColorPrimary },
|
||||
{ token: 'keyword.flow', foreground: themeName.euiColorWarning },
|
||||
{ token: 'keyword.flow.scss', foreground: themeName.euiColorPrimary },
|
||||
|
||||
{ token: 'operator.scss', foreground: themeName.euiColorDarkShade },
|
||||
{ token: 'operator.sql', foreground: themeName.euiColorMediumShade },
|
||||
{ token: 'operator.swift', foreground: themeName.euiColorMediumShade },
|
||||
{ token: 'predefined.sql', foreground: themeName.euiColorMediumShade },
|
||||
],
|
||||
colors: {
|
||||
'editor.foreground': themeColors.foreground,
|
||||
'editor.background': themeColors.editorBackground,
|
||||
'editorLineNumber.foreground': themeColors.lineNumbers,
|
||||
'editorLineNumber.activeForeground': themeColors.lineNumbers,
|
||||
'editorIndentGuide.background': themeColors.editorIndentGuide,
|
||||
'editor.selectionBackground': themeColors.selectionBackground,
|
||||
'editorWidget.border': themeColors.editorWidgetBorder,
|
||||
'editorWidget.background': themeColors.editorWidgetBackground,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { Editor } from './editor';
|
|
@ -10,12 +10,11 @@ import {
|
|||
EuiPanel,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSwitch,
|
||||
EuiRange,
|
||||
EuiToolTip,
|
||||
EuiLink,
|
||||
EuiPortal,
|
||||
} from '@elastic/eui';
|
||||
import { Shortcuts } from 'react-shortcuts';
|
||||
import { ExpressionInput } from '../expression_input';
|
||||
|
@ -23,9 +22,6 @@ import { ToolTipShortcut } from '../tool_tip_shortcut';
|
|||
|
||||
const { useRef } = React;
|
||||
|
||||
const minFontSize = 12;
|
||||
const maxFontSize = 32;
|
||||
|
||||
const shortcut = (ref, cmd, callback) => (
|
||||
<Shortcuts
|
||||
name="EXPRESSION"
|
||||
|
@ -48,112 +44,100 @@ export const Expression = ({
|
|||
setExpression,
|
||||
done,
|
||||
error,
|
||||
isAutocompleteEnabled,
|
||||
toggleAutocompleteEnabled,
|
||||
fontSize,
|
||||
setFontSize,
|
||||
isCompact,
|
||||
toggleCompactView,
|
||||
}) => {
|
||||
const refExpressionInput = useRef(null);
|
||||
return (
|
||||
|
||||
const handleRun = () => {
|
||||
setExpression(formState.expression);
|
||||
// If fullScreen and you hit run, toggle back down so you can see your work
|
||||
if (!isCompact && !error) {
|
||||
toggleCompactView();
|
||||
}
|
||||
};
|
||||
|
||||
const expressionPanel = (
|
||||
<EuiPanel
|
||||
className={`canvasTray__panel canvasExpression--${isCompact ? 'compactSize' : 'fullSize'}`}
|
||||
className={`canvasTray__panel canvasTray__panel--holdingExpression canvasExpression--${
|
||||
isCompact ? 'compactSize' : 'fullSize'
|
||||
}`}
|
||||
paddingSize="none"
|
||||
>
|
||||
{shortcut(refExpressionInput, 'RUN', () => {
|
||||
if (!error) {
|
||||
setExpression(formState.expression);
|
||||
}
|
||||
})}
|
||||
|
||||
{/* Error code below is to pass a non breaking space so the editor does not jump */}
|
||||
|
||||
<ExpressionInput
|
||||
ref={refExpressionInput}
|
||||
fontSize={fontSize}
|
||||
isCompact={isCompact}
|
||||
functionDefinitions={functionDefinitions}
|
||||
error={error}
|
||||
error={error ? error : `\u00A0`}
|
||||
value={formState.expression}
|
||||
onChange={updateValue}
|
||||
isAutocompleteEnabled={isAutocompleteEnabled}
|
||||
/>
|
||||
<div className="canvasExpression--controls">
|
||||
<EuiToolTip content={isCompact ? 'Maximize' : 'Minimize'}>
|
||||
<EuiButtonIcon
|
||||
size="s"
|
||||
onClick={toggleCompactView}
|
||||
iconType="expand"
|
||||
color="subdued"
|
||||
aria-label="Toggle expression window height"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</div>
|
||||
<EuiFlexGroup
|
||||
className="canvasExpression--settings"
|
||||
justifyContent="spaceBetween"
|
||||
alignItems="center"
|
||||
gutterSize="l"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSwitch
|
||||
id="autocompleteOptIn"
|
||||
name="popswitch"
|
||||
label="Enable autocomplete"
|
||||
checked={isAutocompleteEnabled}
|
||||
onChange={toggleAutocompleteEnabled}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem style={{ fontSize: `${minFontSize}px` }} grow={false}>
|
||||
A
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiRange
|
||||
value={fontSize}
|
||||
min={minFontSize}
|
||||
step={4}
|
||||
max={maxFontSize}
|
||||
onChange={e => setFontSize(e.target.value)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ fontSize: `${maxFontSize}px` }}>
|
||||
A
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
color={formState.dirty ? 'danger' : 'primary'}
|
||||
onClick={done}
|
||||
>
|
||||
{formState.dirty ? 'Cancel' : 'Close'}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<span>
|
||||
Run the expression <ToolTipShortcut namespace="EXPRESSION" action="RUN" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<EuiButton
|
||||
fill
|
||||
disabled={!!error}
|
||||
onClick={() => setExpression(formState.expression)}
|
||||
size="s"
|
||||
<div className="canvasExpression__settings">
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<span>
|
||||
Run the expression <ToolTipShortcut namespace="EXPRESSION" action="RUN" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
Run
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiButton fill disabled={!!error} onClick={handleRun} size="s">
|
||||
Run
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
color={formState.dirty ? 'danger' : 'primary'}
|
||||
onClick={done}
|
||||
>
|
||||
{formState.dirty ? 'Cancel' : 'Close'}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/kibana/current/canvas-function-reference.html"
|
||||
target="_blank"
|
||||
>
|
||||
Learn expression syntax
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="fullScreen" onClick={toggleCompactView} size="xs">
|
||||
{isCompact ? 'Maximize' : 'Minimize'} editor
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
||||
if (isCompact) {
|
||||
return expressionPanel;
|
||||
} else {
|
||||
// Portal is required to show above the navigation
|
||||
return <EuiPortal>{expressionPanel}</EuiPortal>;
|
||||
}
|
||||
};
|
||||
|
||||
Expression.propTypes = {
|
||||
|
@ -163,6 +147,4 @@ Expression.propTypes = {
|
|||
setExpression: PropTypes.func,
|
||||
done: PropTypes.func,
|
||||
error: PropTypes.string,
|
||||
isAutocompleteEnabled: PropTypes.bool,
|
||||
toggleAutocompleteEnabled: PropTypes.func,
|
||||
};
|
||||
|
|
|
@ -1,56 +1,47 @@
|
|||
.canvasExpressionInput {
|
||||
// Hack needed because weird editor layout
|
||||
.euiFormErrorText {
|
||||
padding-left: $euiSize;
|
||||
}
|
||||
}
|
||||
|
||||
.canvasExpression--compactSize {
|
||||
max-height: 480px;
|
||||
}
|
||||
|
||||
.canvasExpression--controls {
|
||||
position: absolute;
|
||||
right: $euiSizeXL;
|
||||
top: $euiSizeL;
|
||||
.canvasExpressionInput__editor {
|
||||
height: $euiSize * 16;
|
||||
padding-top: $euiSize;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.canvasExpression--fullSize {
|
||||
height: calc(100vh - 200px); // space for global nav and autocomplete popup
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: $euiZMask - 1;
|
||||
|
||||
.expressionInput {
|
||||
.canvasExpressionInput {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.expressionInput--inner {
|
||||
.canvasExpressionInput__inner {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.canvasExpression--settings {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.canvasTextArea--code {
|
||||
.canvasTextArea__code {
|
||||
flex-grow: 1;
|
||||
padding-right: $euiSizeXXL;
|
||||
}
|
||||
|
||||
.autocomplete {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.canvasAutocomplete--inner {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.autocompletePopup {
|
||||
top: -102px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.autocompleteItems,
|
||||
.autocompleteReference {
|
||||
height: 98px;
|
||||
}
|
||||
}
|
||||
|
||||
.canvasExpression__settings {
|
||||
padding: 0 $euiSize $euiSize;
|
||||
}
|
||||
|
|
|
@ -15,20 +15,12 @@ import {
|
|||
renderComponent,
|
||||
} from 'recompose';
|
||||
import { fromExpression } from '@kbn/interpreter/common';
|
||||
import { Storage } from 'ui/storage';
|
||||
import { getSelectedPage, getSelectedElement } from '../../state/selectors/workpad';
|
||||
import { setExpression, flushContext } from '../../state/actions/elements';
|
||||
import { getFunctionDefinitions } from '../../lib/function_definitions';
|
||||
import { getWindow } from '../../lib/get_window';
|
||||
import {
|
||||
LOCALSTORAGE_AUTOCOMPLETE_ENABLED,
|
||||
LOCALSTORAGE_EXPRESSION_EDITOR_FONT_SIZE,
|
||||
} from '../../../common/lib/constants';
|
||||
import { ElementNotSelected } from './element_not_selected';
|
||||
import { Expression as Component } from './expression';
|
||||
|
||||
const storage = new Storage(getWindow().localStorage);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
pageId: getSelectedPage(state),
|
||||
element: getSelectedElement(state),
|
||||
|
@ -88,20 +80,8 @@ export const Expression = compose(
|
|||
expression,
|
||||
dirty: false,
|
||||
})),
|
||||
withState('isAutocompleteEnabled', 'setIsAutocompleteEnabled', () => {
|
||||
const setting = storage.get(LOCALSTORAGE_AUTOCOMPLETE_ENABLED);
|
||||
return setting === null ? true : setting;
|
||||
}),
|
||||
withState('fontSize', 'setFontSize', () => {
|
||||
const fontSize = storage.get(LOCALSTORAGE_EXPRESSION_EDITOR_FONT_SIZE);
|
||||
return fontSize === null ? 16 : fontSize;
|
||||
}),
|
||||
withState('isCompact', 'setCompact', true),
|
||||
withHandlers({
|
||||
toggleAutocompleteEnabled: ({ isAutocompleteEnabled, setIsAutocompleteEnabled }) => () => {
|
||||
storage.set(LOCALSTORAGE_AUTOCOMPLETE_ENABLED, !isAutocompleteEnabled);
|
||||
setIsAutocompleteEnabled(!isAutocompleteEnabled);
|
||||
},
|
||||
toggleCompactView: ({ isCompact, setCompact }) => () => {
|
||||
setCompact(!isCompact);
|
||||
},
|
||||
|
@ -118,10 +98,6 @@ export const Expression = compose(
|
|||
}));
|
||||
setExpression(exp);
|
||||
},
|
||||
setFontSize: ({ setFontSize }) => size => {
|
||||
storage.set(LOCALSTORAGE_EXPRESSION_EDITOR_FONT_SIZE, size);
|
||||
setFontSize(size);
|
||||
},
|
||||
}),
|
||||
expressionLifecycle,
|
||||
withPropsOnChange(['formState'], ({ formState }) => ({
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Storyshots components/ExpressionInput default 1`] = `
|
||||
<div
|
||||
className="canvasExpressionInput"
|
||||
>
|
||||
<div
|
||||
className="euiFormRow euiFormRow--fullWidth canvasExpressionInput__inner"
|
||||
id="generated-id-row"
|
||||
>
|
||||
<div
|
||||
className="canvasExpressionInput__editor"
|
||||
id="generated-id"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="react-monaco-editor-container"
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
import { ExpressionInput } from '../expression_input';
|
||||
import { language, LANGUAGE_ID } from '../../../lib/monaco_language_def';
|
||||
|
||||
const sampleFunctionDef = {
|
||||
name: 'markdown',
|
||||
type: 'render',
|
||||
aliases: [],
|
||||
help:
|
||||
'Adds an element that renders Markdown text. TIP: Use the `markdown` function for single numbers, metrics, and paragraphs of text.',
|
||||
args: {
|
||||
content: {
|
||||
name: 'content',
|
||||
required: false,
|
||||
help:
|
||||
'A string of text that contains Markdown. To concatenate, pass the `string` function multiple times.',
|
||||
types: ['string'],
|
||||
default: '""',
|
||||
aliases: ['_', 'expression'],
|
||||
multi: true,
|
||||
resolve: false,
|
||||
options: [],
|
||||
},
|
||||
font: {
|
||||
name: 'font',
|
||||
required: false,
|
||||
help: 'The CSS font properties for the content. For example, font-family or font-weight.',
|
||||
types: ['style'],
|
||||
default: '{font}',
|
||||
aliases: [],
|
||||
multi: false,
|
||||
resolve: true,
|
||||
options: [],
|
||||
},
|
||||
},
|
||||
context: {
|
||||
types: ['datatable', 'null'],
|
||||
},
|
||||
|
||||
fn: () => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
language.keywords = [sampleFunctionDef.name];
|
||||
|
||||
monaco.languages.register({ id: LANGUAGE_ID });
|
||||
monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, language);
|
||||
|
||||
storiesOf('components/ExpressionInput', module).add('default', () => (
|
||||
<ExpressionInput
|
||||
value="markdown"
|
||||
isCompact={true}
|
||||
onChange={action('onChange')}
|
||||
functionDefinitions={[sampleFunctionDef as any]}
|
||||
/>
|
||||
));
|
|
@ -1,50 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Markdown from 'markdown-it';
|
||||
import { EuiTitle, EuiText, EuiSpacer, EuiDescriptionList } from '@elastic/eui';
|
||||
|
||||
const md = new Markdown();
|
||||
|
||||
export const ArgumentReference = ({ argDef }) => (
|
||||
<div>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{argDef.name}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiText dangerouslySetInnerHTML={getHelp(argDef)} />
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList type="inline" compressed listItems={getArgListItems(argDef)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
function getHelp(argDef) {
|
||||
return { __html: md.render(argDef.help) };
|
||||
}
|
||||
|
||||
function getArgListItems(argDef) {
|
||||
const { aliases, types, default: def, required } = argDef;
|
||||
const items = [];
|
||||
if (aliases.length) {
|
||||
items.push({ title: 'Aliases', description: aliases.join(', ') });
|
||||
}
|
||||
if (types.length) {
|
||||
items.push({ title: 'Types', description: types.join(', ') });
|
||||
}
|
||||
if (def != null) {
|
||||
items.push({ title: 'Default', description: def });
|
||||
}
|
||||
items.push({ title: 'Required', description: String(Boolean(required)) });
|
||||
return items;
|
||||
}
|
||||
|
||||
ArgumentReference.propTypes = {
|
||||
argDef: PropTypes.object,
|
||||
};
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EuiTextArea, EuiFormRow, EuiTitle } from '@elastic/eui';
|
||||
import { debounce, startCase } from 'lodash';
|
||||
import { Autocomplete } from '../autocomplete';
|
||||
import {
|
||||
getAutocompleteSuggestions,
|
||||
getFnArgDefAtPosition,
|
||||
} from '../../../common/lib/autocomplete';
|
||||
import { FunctionReference } from './function_reference';
|
||||
import { ArgumentReference } from './argument_reference';
|
||||
|
||||
export class ExpressionInput extends React.Component {
|
||||
constructor({ value }) {
|
||||
super();
|
||||
|
||||
this.undoHistory = [];
|
||||
this.redoHistory = [];
|
||||
|
||||
this.state = {
|
||||
selection: {
|
||||
start: value.length,
|
||||
end: value.length,
|
||||
},
|
||||
suggestions: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!this.ref) {
|
||||
return;
|
||||
}
|
||||
const { selection } = this.state;
|
||||
const { start, end } = selection;
|
||||
this.ref.setSelectionRange(start, end);
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (!this.undoHistory.length) {
|
||||
return;
|
||||
}
|
||||
const value = this.undoHistory.pop();
|
||||
this.redoHistory.push(this.props.value);
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (!this.redoHistory.length) {
|
||||
return;
|
||||
}
|
||||
const value = this.redoHistory.pop();
|
||||
this.undoHistory.push(this.props.value);
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
getSelection() {
|
||||
if (!this.ref) {
|
||||
return null;
|
||||
}
|
||||
const start = this.ref.selectionStart;
|
||||
const finish = this.ref.selectionEnd;
|
||||
return this.ref.value.substring(start, finish);
|
||||
}
|
||||
|
||||
stash = debounce(
|
||||
value => {
|
||||
this.undoHistory.push(value);
|
||||
this.redoHistory = [];
|
||||
},
|
||||
500,
|
||||
{ leading: true, trailing: false }
|
||||
);
|
||||
|
||||
onKeyDown = e => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === 'z') {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
this.redo();
|
||||
} else {
|
||||
this.undo();
|
||||
}
|
||||
}
|
||||
if (e.key === 'y') {
|
||||
e.preventDefault();
|
||||
this.redo();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onSuggestionSelect = item => {
|
||||
const { text, start, end } = item;
|
||||
const value = this.props.value.substr(0, start) + text + this.props.value.substr(end);
|
||||
const selection = { start: start + text.length, end: start + text.length };
|
||||
this.updateState({ value, selection });
|
||||
|
||||
// This is needed for when the suggestion was selected by clicking on it
|
||||
this.ref.focus();
|
||||
};
|
||||
|
||||
onChange = e => {
|
||||
const { target } = e;
|
||||
const { value, selectionStart, selectionEnd } = target;
|
||||
const selection = {
|
||||
start: selectionStart,
|
||||
end: selectionEnd,
|
||||
};
|
||||
this.updateState({ value, selection });
|
||||
};
|
||||
|
||||
updateState = ({ value, selection }) => {
|
||||
this.stash(this.props.value);
|
||||
const suggestions = getAutocompleteSuggestions(
|
||||
this.props.functionDefinitions,
|
||||
value,
|
||||
selection.start
|
||||
);
|
||||
this.props.onChange(value);
|
||||
this.setState({ selection, suggestions });
|
||||
};
|
||||
|
||||
getHeader = () => {
|
||||
const { suggestions } = this.state;
|
||||
if (!suggestions.length) {
|
||||
return '';
|
||||
}
|
||||
return (
|
||||
<EuiTitle className="autocompleteType" size="xs">
|
||||
<h3>{startCase(suggestions[0].type)}</h3>
|
||||
</EuiTitle>
|
||||
);
|
||||
};
|
||||
|
||||
getReference = selectedItem => {
|
||||
const { fnDef, argDef } = selectedItem || {};
|
||||
if (argDef) {
|
||||
return <ArgumentReference argDef={argDef} />;
|
||||
}
|
||||
if (fnDef) {
|
||||
return <FunctionReference fnDef={fnDef} />;
|
||||
}
|
||||
|
||||
const { fnDef: fnDefAtPosition, argDef: argDefAtPosition } = getFnArgDefAtPosition(
|
||||
this.props.functionDefinitions,
|
||||
this.props.value,
|
||||
this.state.selection.start
|
||||
);
|
||||
|
||||
if (argDefAtPosition) {
|
||||
return <ArgumentReference argDef={argDefAtPosition} />;
|
||||
}
|
||||
if (fnDefAtPosition) {
|
||||
return <FunctionReference fnDef={fnDefAtPosition} />;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, error, isAutocompleteEnabled, fontSize } = this.props;
|
||||
const { suggestions } = this.state;
|
||||
|
||||
const helpText = error
|
||||
? null
|
||||
: 'This is the coded expression that backs this element. You better know what you are doing here.';
|
||||
return (
|
||||
<div className="expressionInput">
|
||||
<EuiFormRow
|
||||
className="expressionInput--inner"
|
||||
fullWidth
|
||||
isInvalid={Boolean(error)}
|
||||
error={error}
|
||||
helpText={helpText}
|
||||
>
|
||||
{isAutocompleteEnabled ? (
|
||||
<Autocomplete
|
||||
header={this.getHeader()}
|
||||
items={suggestions}
|
||||
onSelect={this.onSuggestionSelect}
|
||||
reference={this.getReference}
|
||||
>
|
||||
<EuiTextArea
|
||||
onKeyDown={this.onKeyDown}
|
||||
className="canvasTextArea--code"
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
inputRef={ref => (this.ref = ref)}
|
||||
spellCheck="false"
|
||||
style={{ fontSize: `${fontSize}px` }}
|
||||
resize="none"
|
||||
/>
|
||||
</Autocomplete>
|
||||
) : (
|
||||
<EuiTextArea
|
||||
onKeyDown={this.onKeyDown}
|
||||
className="canvasTextArea--code"
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
inputRef={ref => (this.ref = ref)}
|
||||
spellCheck="false"
|
||||
style={{ fontSize: `${fontSize}px` }}
|
||||
resize="none"
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExpressionInput.propTypes = {
|
||||
functionDefinitions: PropTypes.array,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
error: PropTypes.string,
|
||||
isAutocompleteEnabled: PropTypes.bool,
|
||||
};
|
|
@ -0,0 +1,280 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { debounce } from 'lodash';
|
||||
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
import { Editor } from '../editor';
|
||||
|
||||
import { CanvasFunction } from '../../../types';
|
||||
import {
|
||||
AutocompleteSuggestion,
|
||||
getAutocompleteSuggestions,
|
||||
getFnArgDefAtPosition,
|
||||
} from '../../../common/lib/autocomplete';
|
||||
|
||||
import { LANGUAGE_ID } from '../../lib/monaco_language_def';
|
||||
|
||||
import { getFunctionReferenceStr, getArgReferenceStr } from './reference';
|
||||
|
||||
interface Props {
|
||||
/** Font size of text within the editor */
|
||||
|
||||
/** Canvas function defintions */
|
||||
functionDefinitions: CanvasFunction[];
|
||||
|
||||
/** Optional string for displaying error messages */
|
||||
error?: string;
|
||||
/** Value of expression */
|
||||
value: string;
|
||||
/** Function invoked when expression value is changed */
|
||||
onChange: (value?: string) => void;
|
||||
/** In full screen mode or not */
|
||||
isCompact: boolean;
|
||||
}
|
||||
|
||||
export class ExpressionInput extends React.Component<Props> {
|
||||
static propTypes = {
|
||||
functionDefinitions: PropTypes.array.isRequired,
|
||||
|
||||
value: PropTypes.string.isRequired,
|
||||
error: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
undoHistory: string[];
|
||||
redoHistory: string[];
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.undoHistory = [];
|
||||
this.redoHistory = [];
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (!this.undoHistory.length) {
|
||||
return;
|
||||
}
|
||||
const value = this.undoHistory.pop();
|
||||
this.redoHistory.push(this.props.value);
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (!this.redoHistory.length) {
|
||||
return;
|
||||
}
|
||||
const value = this.redoHistory.pop();
|
||||
this.undoHistory.push(this.props.value);
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
stash = debounce(
|
||||
(value: string) => {
|
||||
this.undoHistory.push(value);
|
||||
this.redoHistory = [];
|
||||
},
|
||||
500,
|
||||
{ leading: true, trailing: false }
|
||||
);
|
||||
onKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === 'z') {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
this.redo();
|
||||
} else {
|
||||
this.undo();
|
||||
}
|
||||
}
|
||||
if (e.key === 'y') {
|
||||
e.preventDefault();
|
||||
this.redo();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onChange = (value: string) => {
|
||||
this.updateState({ value });
|
||||
};
|
||||
|
||||
updateState = ({ value }: { value: string }) => {
|
||||
this.stash(this.props.value);
|
||||
|
||||
this.props.onChange(value);
|
||||
};
|
||||
|
||||
provideSuggestions = (model: monacoEditor.editor.ITextModel, position: monacoEditor.Position) => {
|
||||
const text = model.getValue();
|
||||
const textRange = model.getFullModelRange();
|
||||
|
||||
const lengthAfterPosition = model.getValueLengthInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: position.column,
|
||||
endLineNumber: textRange.endLineNumber,
|
||||
endColumn: textRange.endColumn,
|
||||
});
|
||||
|
||||
const wordUntil = model.getWordUntilPosition(position);
|
||||
const wordRange = new monacoEditor.Range(
|
||||
position.lineNumber,
|
||||
wordUntil.startColumn,
|
||||
position.lineNumber,
|
||||
wordUntil.endColumn
|
||||
);
|
||||
|
||||
const aSuggestions = getAutocompleteSuggestions(
|
||||
this.props.functionDefinitions,
|
||||
text,
|
||||
text.length - lengthAfterPosition
|
||||
);
|
||||
|
||||
const suggestions = aSuggestions.map((s: AutocompleteSuggestion) => {
|
||||
if (s.type === 'argument') {
|
||||
return {
|
||||
label: s.argDef.name,
|
||||
kind: monacoEditor.languages.CompletionItemKind.Field,
|
||||
documentation: { value: getArgReferenceStr(s.argDef), isTrusted: true },
|
||||
insertText: s.text,
|
||||
command: {
|
||||
title: 'Trigger Suggestion Dialog',
|
||||
id: 'editor.action.triggerSuggest',
|
||||
},
|
||||
range: wordRange,
|
||||
};
|
||||
} else if (s.type === 'value') {
|
||||
return {
|
||||
label: s.text,
|
||||
kind: monacoEditor.languages.CompletionItemKind.Value,
|
||||
insertText: s.text,
|
||||
command: {
|
||||
title: 'Trigger Suggestion Dialog',
|
||||
id: 'editor.action.triggerSuggest',
|
||||
},
|
||||
range: wordRange,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
label: s.fnDef.name,
|
||||
kind: monacoEditor.languages.CompletionItemKind.Function,
|
||||
documentation: {
|
||||
value: getFunctionReferenceStr(s.fnDef),
|
||||
isTrusted: true,
|
||||
},
|
||||
insertText: s.text,
|
||||
command: {
|
||||
title: 'Trigger Suggestion Dialog',
|
||||
id: 'editor.action.triggerSuggest',
|
||||
},
|
||||
range: wordRange,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
};
|
||||
};
|
||||
|
||||
providerHover = (model: monacoEditor.editor.ITextModel, position: monacoEditor.Position) => {
|
||||
const text = model.getValue();
|
||||
const word = model.getWordAtPosition(position);
|
||||
|
||||
if (!word) {
|
||||
return {
|
||||
contents: [],
|
||||
};
|
||||
}
|
||||
|
||||
const absPosition = model.getValueLengthInRange({
|
||||
startLineNumber: 0,
|
||||
startColumn: 0,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: word.endColumn,
|
||||
});
|
||||
|
||||
const { fnDef, argDef, argStart, argEnd } = getFnArgDefAtPosition(
|
||||
this.props.functionDefinitions,
|
||||
text,
|
||||
absPosition
|
||||
);
|
||||
|
||||
if (argDef && argStart && argEnd) {
|
||||
// Use the start/end position of the arg to generate a complete range to highlight
|
||||
// that includes the arg name and its complete value
|
||||
const startPos = model.getPositionAt(argStart);
|
||||
const endPos = model.getPositionAt(argEnd);
|
||||
|
||||
const argRange = new monacoEditor.Range(
|
||||
startPos.lineNumber,
|
||||
startPos.column,
|
||||
endPos.lineNumber,
|
||||
endPos.column
|
||||
);
|
||||
|
||||
return {
|
||||
contents: [{ value: getArgReferenceStr(argDef), isTrusted: true }],
|
||||
range: argRange,
|
||||
};
|
||||
} else if (fnDef) {
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
value: getFunctionReferenceStr(fnDef),
|
||||
isTrusted: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
contents: [],
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, error, isCompact } = this.props;
|
||||
|
||||
return (
|
||||
<div className="canvasExpressionInput">
|
||||
<EuiFormRow
|
||||
className="canvasExpressionInput__inner"
|
||||
fullWidth
|
||||
isInvalid={Boolean(error)}
|
||||
error={error}
|
||||
>
|
||||
<div className="canvasExpressionInput__editor">
|
||||
<Editor
|
||||
languageId={LANGUAGE_ID}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
suggestionProvider={{
|
||||
triggerCharacters: [' '],
|
||||
provideCompletionItems: this.provideSuggestions,
|
||||
}}
|
||||
hoverProvider={{
|
||||
provideHover: this.providerHover,
|
||||
}}
|
||||
options={{
|
||||
fontSize: isCompact ? 12 : 16,
|
||||
scrollBeyondLastLine: false,
|
||||
quickSuggestions: true,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
wordBasedSuggestions: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,82 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Markdown from 'markdown-it';
|
||||
import { EuiTitle, EuiText, EuiSpacer, EuiBasicTable, EuiDescriptionList } from '@elastic/eui';
|
||||
import { startCase } from 'lodash';
|
||||
|
||||
const md = new Markdown();
|
||||
|
||||
export const FunctionReference = ({ fnDef }) => (
|
||||
<div>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{fnDef.name}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiText dangerouslySetInnerHTML={getHelp(fnDef)} />
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiDescriptionList
|
||||
type="inline"
|
||||
className="autocompleteDescList"
|
||||
compressed
|
||||
listItems={getFnListItems(fnDef)}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiBasicTable
|
||||
className="autocompleteTable"
|
||||
items={getArgItems(fnDef.args)}
|
||||
columns={getArgColumns()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
function getHelp(fnDef) {
|
||||
return { __html: md.render(fnDef.help) };
|
||||
}
|
||||
|
||||
function getFnListItems(fnDef) {
|
||||
const { aliases, context, type } = fnDef;
|
||||
const items = [];
|
||||
if (aliases.length) {
|
||||
items.push({ title: 'Aliases', description: aliases.join(', ') });
|
||||
}
|
||||
if (context.types) {
|
||||
items.push({ title: 'Accepts', description: context.types.join(', ') });
|
||||
}
|
||||
if (type) {
|
||||
items.push({ title: 'Returns', description: type });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function getArgItems(args) {
|
||||
return Object.entries(args).map(([name, argDef]) => ({
|
||||
argument: name + (argDef.required ? '*' : ''),
|
||||
aliases: (argDef.aliases || []).join(', '),
|
||||
types: (argDef.types || []).join(', '),
|
||||
default: argDef.default || '',
|
||||
description: argDef.help || '',
|
||||
}));
|
||||
}
|
||||
|
||||
function getArgColumns() {
|
||||
return ['argument', 'aliases', 'types', 'default', 'description'].map(field => {
|
||||
const column = { field, name: startCase(field), truncateText: field !== 'description' };
|
||||
if (field === 'description') {
|
||||
column.width = '50%';
|
||||
}
|
||||
return column;
|
||||
});
|
||||
}
|
||||
|
||||
FunctionReference.propTypes = {
|
||||
fnDef: PropTypes.object,
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { CanvasFunction, CanvasArgValue } from '../../../types';
|
||||
|
||||
/**
|
||||
* Given a function definition, this function returns a markdown string
|
||||
* that includes the context the function accepts, what the function returns
|
||||
* as well as the general help/documentation text associated with the function
|
||||
*/
|
||||
export function getFunctionReferenceStr(fnDef: CanvasFunction) {
|
||||
const { help, context, type } = fnDef;
|
||||
const doc = `**Accepts**: ${
|
||||
context && context.types ? context.types.join(' | ') : 'null'
|
||||
}, **Returns**: ${type ? type : 'null'}
|
||||
\n\n${help}`;
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an argument defintion, this function returns a markdown string
|
||||
* that includes the aliases of the argument, types accepted for the argument,
|
||||
* the default value of the argument, whether or not its required, and
|
||||
* the general help/documentation text associated with the argument
|
||||
*/
|
||||
export function getArgReferenceStr(argDef: CanvasArgValue) {
|
||||
const { aliases, types, default: def, required, help } = argDef;
|
||||
|
||||
const ref = `**Aliases**: ${
|
||||
aliases && aliases.length ? aliases.join(' | ') : 'null'
|
||||
}, **Types**: ${types && types.length ? types.join(' | ') : 'null'}
|
||||
\n\n${def != null ? '**Default**: ' + def + ', ' : ''}**Required**: ${String(Boolean(required))}
|
||||
\n\n${help}`;
|
||||
|
||||
return ref;
|
||||
}
|
|
@ -6,4 +6,9 @@
|
|||
.canvasTray__panel {
|
||||
background-color: $euiPageBackgroundColor;
|
||||
border-radius: 0;
|
||||
|
||||
&.canvasTray__panel--holdingExpression {
|
||||
background-color: $euiColorEmptyShade;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
103
x-pack/legacy/plugins/canvas/public/lib/monaco_language_def.ts
Normal file
103
x-pack/legacy/plugins/canvas/public/lib/monaco_language_def.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
// @ts-ignore
|
||||
import { registries } from 'plugins/interpreter/registries';
|
||||
|
||||
import { CanvasFunction } from '../../types';
|
||||
|
||||
export const LANGUAGE_ID = 'canvas-expression';
|
||||
|
||||
/**
|
||||
* Extends the default type for a Monarch language so we can use
|
||||
* attribute references (like @keywords to reference the keywords list)
|
||||
* in the defined tokenizer
|
||||
*/
|
||||
interface Language extends monaco.languages.IMonarchLanguage {
|
||||
keywords: string[];
|
||||
symbols: RegExp;
|
||||
escapes: RegExp;
|
||||
digits: RegExp;
|
||||
boolean: ['true', 'false'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the Monarch tokenizer for syntax highlighting in Monaco of the
|
||||
* expression language. The tokenizer defines a set of regexes and actions/tokens
|
||||
* to mark the detected words/characters.
|
||||
* For more information, the Monarch documentation can be found here:
|
||||
* https://microsoft.github.io/monaco-editor/monarch.html
|
||||
*/
|
||||
export const language: Language = {
|
||||
keywords: [],
|
||||
|
||||
symbols: /[=|]/,
|
||||
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
|
||||
digits: /\d+(_+\d+)*/,
|
||||
boolean: ['true', 'false'],
|
||||
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/[{}]/, 'delimiter.bracket'],
|
||||
{
|
||||
include: 'common',
|
||||
},
|
||||
],
|
||||
|
||||
common: [
|
||||
// identifiers and keywords
|
||||
[
|
||||
/[a-z_$][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@keywords': 'keyword',
|
||||
'@boolean': 'keyword',
|
||||
'@default': 'identifier',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
[/(@digits)/, 'number'],
|
||||
|
||||
[/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string
|
||||
[/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string
|
||||
[/"/, 'string', '@string_double'],
|
||||
[/'/, 'string', '@string_single'],
|
||||
|
||||
[/@symbols/, 'delimiter'],
|
||||
],
|
||||
|
||||
string_double: [
|
||||
[/[^\\"]+/, 'string'],
|
||||
[/@escapes/, 'string.escape'],
|
||||
[/\\./, 'string.escape.invalid'],
|
||||
[/"/, 'string', '@pop'],
|
||||
],
|
||||
|
||||
string_single: [
|
||||
[/[^\\']+/, 'string'],
|
||||
[/@escapes/, 'string.escape'],
|
||||
[/\\./, 'string.escape.invalid'],
|
||||
[/'/, 'string', '@pop'],
|
||||
],
|
||||
|
||||
bracketCounting: [
|
||||
[/\{/, 'delimiter.bracket', '@bracketCounting'],
|
||||
[/\}/, 'delimiter.bracket', '@pop'],
|
||||
{ include: 'common' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function registerLanguage() {
|
||||
const functions = registries.browserFunctions.toArray();
|
||||
language.keywords = functions.map((fn: CanvasFunction) => fn.name);
|
||||
|
||||
monaco.languages.register({ id: LANGUAGE_ID });
|
||||
monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, language);
|
||||
}
|
|
@ -30,6 +30,7 @@
|
|||
@import '../components/debug/debug';
|
||||
@import '../components/dom_preview/dom_preview';
|
||||
@import '../components/dragbox_annotation/dragbox_annotation';
|
||||
@import '../components/editor/editor';
|
||||
@import '../components/element_card/element_card';
|
||||
@import '../components/element_content/element_content';
|
||||
@import '../components/expression/expression';
|
||||
|
|
|
@ -19,15 +19,17 @@ $canvasElementCardWidth: 210px;
|
|||
|
||||
.canvasCheckered {
|
||||
background-color: $euiColorGhost;
|
||||
|
||||
// sass-lint:disable-block indentation
|
||||
background-image: linear-gradient(45deg, $euiColorLightShade 25%, transparent 25%),
|
||||
linear-gradient(-45deg, $euiColorLightShade 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, $euiColorLightShade 75%),
|
||||
linear-gradient(-45deg, transparent 75%, $euiColorLightShade 75%);
|
||||
linear-gradient(-45deg, $euiColorLightShade 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, $euiColorLightShade 75%),
|
||||
linear-gradient(-45deg, transparent 75%, $euiColorLightShade 75%);
|
||||
background-size: $euiSizeS $euiSizeS;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.canvasTextArea--code {
|
||||
.canvasTextArea__code {
|
||||
@include euiScrollBar;
|
||||
font-size: $euiFontSize;
|
||||
font-family: $euiCodeFontFamily;
|
||||
|
@ -39,7 +41,8 @@ $canvasElementCardWidth: 210px;
|
|||
border-top: $euiBorderThin;
|
||||
}
|
||||
|
||||
#canvas-app { // sass-lint:disable-line no-ids
|
||||
// sass-lint:disable-block no-ids
|
||||
#canvas-app {
|
||||
overflow-y: hidden;
|
||||
|
||||
.window-error {
|
||||
|
|
|
@ -7,4 +7,11 @@
|
|||
export default {
|
||||
getBasePath: () => '/abc',
|
||||
trackSubUrlForApp: () => undefined, // noop
|
||||
getUiSettingsClient: () => {
|
||||
return {
|
||||
get: () => {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -25,6 +25,9 @@ export type UnionToIntersection<U> =
|
|||
*/
|
||||
export type ValuesOf<T extends any[]> = T[number];
|
||||
|
||||
type valueof<T> = T[keyof T];
|
||||
type ValuesOfUnion<T> = T extends any ? valueof<T> : never;
|
||||
|
||||
/**
|
||||
* A `ExpressionFunctionFactory` is a powerful type used for any function that produces
|
||||
* an `ExpressionFunction`. If it does not meet the signature for such a function,
|
||||
|
@ -116,6 +119,13 @@ export type CanvasFunction = FunctionFactory<Functions>;
|
|||
*/
|
||||
export type CanvasFunctionName = CanvasFunction['name'];
|
||||
|
||||
/**
|
||||
* A union type of all Canvas Function argument objects.
|
||||
*/
|
||||
export type CanvasArg = CanvasFunction['args'];
|
||||
|
||||
export type CanvasArgValue = ValuesOfUnion<CanvasFunction['args']>;
|
||||
|
||||
/**
|
||||
* Represents a function called by the `case` Function.
|
||||
*/
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
.monaco-editor .cursors-layer > .cursor {
|
||||
display: none !important;
|
||||
}
|
||||
.codeContainer__editor {
|
||||
.monaco-editor .cursors-layer > .cursor {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
textarea.inputarea {
|
||||
display: none !important;
|
||||
}
|
||||
textarea.inputarea {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.monaco-editor.mac .margin-view-overlays .line-numbers {
|
||||
cursor: pointer;
|
||||
background-color: $euiColorLightestShade;
|
||||
}
|
||||
.monaco-editor.mac .margin-view-overlays .line-numbers {
|
||||
cursor: pointer;
|
||||
background-color: $euiColorLightestShade;
|
||||
}
|
||||
|
||||
.code-line-decoration + .cldr.folding {
|
||||
left: -124px !important;
|
||||
opacity: 1;
|
||||
}
|
||||
.code-line-decoration + .cldr.folding {
|
||||
left: -124px !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
span.mtk6 {
|
||||
color: $euiColorSecondary;
|
||||
}
|
||||
span.mtk6 {
|
||||
color: $euiColorSecondary;
|
||||
}
|
||||
|
||||
span.mtk29 {
|
||||
color: $euiColorAccent;
|
||||
span.mtk29 {
|
||||
color: $euiColorAccent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
"@types/react": "^16.8.0",
|
||||
"@types/react-dom": "^16.8.0",
|
||||
"@types/react-redux": "^6.0.6",
|
||||
"@types/react-resize-detector": "^4.0.1",
|
||||
"@types/react-router-dom": "^4.3.1",
|
||||
"@types/react-sticky": "^6.0.3",
|
||||
"@types/react-test-renderer": "^16.8.0",
|
||||
|
@ -316,9 +317,11 @@
|
|||
"react-markdown": "^3.4.1",
|
||||
"react-markdown-renderer": "^1.4.0",
|
||||
"react-moment-proptypes": "^1.6.0",
|
||||
"react-monaco-editor": "^0.26.2",
|
||||
"react-portal": "^3.2.0",
|
||||
"react-redux": "^5.1.1",
|
||||
"react-redux-request": "^1.5.6",
|
||||
"react-resize-detector": "^4.2.0",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-select": "^1.2.1",
|
||||
"react-shortcuts": "^2.0.0",
|
||||
|
|
27
yarn.lock
27
yarn.lock
|
@ -3871,6 +3871,13 @@
|
|||
"@types/react" "*"
|
||||
redux "^4.0.0"
|
||||
|
||||
"@types/react-resize-detector@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-resize-detector/-/react-resize-detector-4.0.1.tgz#cc8f012f5957e4826e69b8d2afd59baadcac556c"
|
||||
integrity sha512-i115c58mAIXGS4CnDmKv5N5KNbPcABhph7SSfpsfBEjx9KJ0JcYKDeNc73H200Eo7vVSFnAIIDs7EmVWeoZiaw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-router-dom@^4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.1.tgz#71fe2918f8f60474a891520def40a63997dafe04"
|
||||
|
@ -23096,6 +23103,15 @@ react-moment-proptypes@^1.6.0:
|
|||
dependencies:
|
||||
moment ">=1.6.0"
|
||||
|
||||
react-monaco-editor@^0.26.2:
|
||||
version "0.26.2"
|
||||
resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.26.2.tgz#a12b188529cb9ca4859a9d688ad35701c7d9c933"
|
||||
integrity sha512-a7/w6l8873ankpa5cdAwXSRnwEis8V/2YVeQA0JdTh0edFhQ/2TKlgm8bOFYmGX3taBk+EVp9OMNQvYH1O73iA==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
monaco-editor "^0.17.0"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-motion@^0.4.8:
|
||||
version "0.4.8"
|
||||
resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.4.8.tgz#23bb2dd27c2d8e00d229e45572d105efcf40a35e"
|
||||
|
@ -23222,6 +23238,17 @@ react-resize-detector@^3.2.1:
|
|||
prop-types "^15.6.2"
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
|
||||
react-resize-detector@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-4.2.0.tgz#b87aee6b37c9e8a52daca8736b3230cf6a2a8647"
|
||||
integrity sha512-AtOaNIxs0ydua7tEoglXR3902/EdlIj9PXDu1Zj0ug2VAUnkSQjguLGzaG/N6CXLOhJSccTsUCZxjLayQ1mE9Q==
|
||||
dependencies:
|
||||
lodash "^4.17.11"
|
||||
lodash-es "^4.17.11"
|
||||
prop-types "^15.7.2"
|
||||
raf-schd "^4.0.0"
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
|
||||
react-router-dom@4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue