[presentation] Create Expression Input (#119411) (#120413)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Clint Andrew Hall <clint.hall@elastic.co>
This commit is contained in:
Kibana Machine 2021-12-03 17:05:07 -05:00 committed by GitHub
parent 36d2ab49f7
commit 97e3bbd891
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 662 additions and 656 deletions

View file

@ -12,4 +12,10 @@
export const PLUGIN_ID = 'presentationUtil';
export const PLUGIN_NAME = 'presentationUtil';
/**
* The unique identifier for the Expressions Language for use in the ExpressionInput
* and CodeEditor components.
*/
export const EXPRESSIONS_LANGUAGE_ID = 'kibana-expressions';
export * from './labs';

View file

@ -9,7 +9,16 @@
"kibanaVersion": "kibana",
"server": true,
"ui": true,
"extraPublicDirs": ["common/lib"],
"requiredPlugins": ["savedObjects", "data", "dataViews", "embeddable", "kibanaReact"],
"extraPublicDirs": [
"common/lib"
],
"requiredPlugins": [
"savedObjects",
"data",
"dataViews",
"embeddable",
"kibanaReact",
"expressions"
],
"optionalPlugins": []
}

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 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 { uniq } from 'lodash';
@ -15,9 +16,9 @@ import {
ExpressionFunction,
ExpressionFunctionParameter,
getByAlias,
} from '../../../../../src/plugins/expressions/common';
} from '../../../../expressions/common';
const MARKER = 'CANVAS_SUGGESTION_MARKER';
const MARKER = 'EXPRESSIONS_SUGGESTION_MARKER';
interface BaseSuggestion {
text: string;
@ -25,11 +26,6 @@ interface BaseSuggestion {
end: number;
}
export interface FunctionSuggestion extends BaseSuggestion {
type: 'function';
fnDef: ExpressionFunction;
}
interface ArgSuggestionValue extends Omit<ExpressionFunctionParameter, 'accepts'> {
name: string;
}
@ -43,8 +39,6 @@ interface ValueSuggestion extends BaseSuggestion {
type: 'value';
}
export type AutocompleteSuggestion = FunctionSuggestion | ArgSuggestion | ValueSuggestion;
interface FnArgAtPosition {
ast: ExpressionASTWithMeta;
fnIndex: number;
@ -57,6 +51,7 @@ interface FnArgAtPosition {
// If this function is a sub-expression function, we need the parent function and argument
// name to determine the return type of the function
parentFn?: string;
// If this function is a sub-expression function, the context could either be local or it
// could be the parent's previous function.
contextFn?: string | null;
@ -101,6 +96,13 @@ type ExpressionASTWithMeta = ASTMetaInformation<
>
>;
export interface FunctionSuggestion extends BaseSuggestion {
type: 'function';
fnDef: ExpressionFunction;
}
export type AutocompleteSuggestion = FunctionSuggestion | ArgSuggestion | ValueSuggestion;
// Typeguard for checking if ExpressionArg is a new expression
function isExpression(
maybeExpression: ExpressionArgASTWithMeta

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { CodeEditorProps } from '../../../../kibana_react/public';
export const LANGUAGE_CONFIGURATION = {
autoClosingPairs: [
{
open: '{',
close: '}',
},
],
};
export const CODE_EDITOR_OPTIONS: CodeEditorProps['options'] = {
scrollBeyondLastLine: false,
quickSuggestions: true,
minimap: {
enabled: false,
},
wordWrap: 'on',
wrappingIndent: 'indent',
};

View file

@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { action } from '@storybook/addon-actions';
import { Meta } from '@storybook/react';
import { ExpressionFunction, ExpressionFunctionParameter, Style } from 'src/plugins/expressions';
import { ExpressionInput } from '../expression_input';
import { registerExpressionsLanguage } from './language';
const content: ExpressionFunctionParameter<'string'> = {
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: [],
accepts: () => true,
};
const font: ExpressionFunctionParameter<Style> = {
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: [],
accepts: () => true,
};
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,
font,
},
fn: () => ({
as: 'markdown',
value: true,
type: 'render',
}),
} as unknown as ExpressionFunction;
registerExpressionsLanguage([sampleFunctionDef]);
export default {
title: 'Expression Input',
description: '',
argTypes: {
isCompact: {
control: 'boolean',
defaultValue: false,
},
},
decorators: [
(storyFn, { globals }) => (
<div
style={{
padding: 40,
backgroundColor:
globals.euiTheme === 'v8.dark' || globals.euiTheme === 'v7.dark' ? '#1D1E24' : '#FFF',
}}
>
{storyFn()}
</div>
),
],
} as Meta;
export const Example = ({ isCompact }: { isCompact: boolean }) => (
<ExpressionInput
expression="markdown"
height={300}
onChange={action('onChange')}
expressionFunctions={[sampleFunctionDef as any]}
{...{ isCompact }}
/>
);

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState, useMemo } from 'react';
import { debounce } from 'lodash';
import type { monaco } from '@kbn/monaco';
import { CodeEditor } from '../../../../kibana_react/public';
import { ExpressionInputProps } from '../types';
import { EXPRESSIONS_LANGUAGE_ID } from '../../../common';
import { CODE_EDITOR_OPTIONS, LANGUAGE_CONFIGURATION } from './constants';
import { getHoverProvider, getSuggestionProvider } from './providers';
/**
* An input component that can provide suggestions and hover information for an Expression
* as it is being written. Be certain to provide ExpressionFunctions by calling `registerExpressionFunctions`
* from the start contract of the presentationUtil plugin.
*/
export const ExpressionInput = (props: ExpressionInputProps) => {
const {
expressionFunctions,
expression: initialExpression,
onChange: onChangeProp,
isCompact,
height,
style,
editorRef,
...rest
} = props;
const [expression, setExpression] = useState(initialExpression);
const suggestionProvider = useMemo(
() => getSuggestionProvider(expressionFunctions),
[expressionFunctions]
);
const hoverProvider = useMemo(() => getHoverProvider(expressionFunctions), [expressionFunctions]);
// Updating tab size for the editor
const editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
const model = editor.getModel();
model?.updateOptions({ tabSize: 2 });
if (editorRef) {
editorRef.current = editor;
}
};
const setValue = debounce((value: string) => setExpression(value), 500, {
leading: true,
trailing: false,
});
const onChange = (value: string) => {
setValue(value);
onChangeProp(value);
};
return (
<div style={{ height, ...style }} {...{ rest }}>
<CodeEditor
languageId={EXPRESSIONS_LANGUAGE_ID}
languageConfiguration={LANGUAGE_CONFIGURATION}
value={expression}
onChange={onChange}
suggestionProvider={suggestionProvider}
hoverProvider={hoverProvider}
options={{
...CODE_EDITOR_OPTIONS,
fontSize: isCompact ? 12 : 16,
}}
editorDidMount={editorDidMount}
/>
</div>
);
};

View file

@ -0,0 +1,16 @@
/*
* 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 { ExpressionInput } from './expression_input';
export { registerExpressionsLanguage } from './language';
import { ExpressionInput } from './expression_input';
// required for dynamic import using React.lazy()
// eslint-disable-next-line import/no-default-export
export default ExpressionInput;

View file

@ -1,21 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 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';
import { ExpressionFunction } from '../../types';
export const LANGUAGE_ID = 'canvas-expression';
import { ExpressionFunction } from 'src/plugins/expressions/common';
import { EXPRESSIONS_LANGUAGE_ID } from '../../../common';
/**
* 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 {
interface ExpressionsLanguage extends monaco.languages.IMonarchLanguage {
keywords: string[];
symbols: RegExp;
escapes: RegExp;
@ -28,10 +28,11 @@ interface Language extends monaco.languages.IMonarchLanguage {
* 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 = {
const expressionsLanguage: ExpressionsLanguage = {
keywords: [],
symbols: /[=|]/,
@ -95,8 +96,8 @@ export const language: Language = {
},
};
export function registerLanguage(functions: ExpressionFunction[]) {
language.keywords = functions.map((fn) => fn.name);
monaco.languages.register({ id: LANGUAGE_ID });
monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, language);
export function registerExpressionsLanguage(functions: ExpressionFunction[]) {
expressionsLanguage.keywords = functions.map((fn) => fn.name);
monaco.languages.register({ id: EXPRESSIONS_LANGUAGE_ID });
monaco.languages.setMonarchTokensProvider(EXPRESSIONS_LANGUAGE_ID, expressionsLanguage);
}

View file

@ -0,0 +1,189 @@
/*
* 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';
import { ExpressionFunction } from '../../../../expressions/common';
import {
AutocompleteSuggestion,
getAutocompleteSuggestions,
getFnArgDefAtPosition,
} from './autocomplete';
import { getFunctionReferenceStr, getArgReferenceStr } from './reference';
export const getSuggestionProvider = (expressionFunctions: ExpressionFunction[]) => {
const provideCompletionItems = (
model: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.CompletionContext
) => {
const text = model.getValue();
const textRange = model.getFullModelRange();
const lengthAfterPosition = model.getValueLengthInRange({
startLineNumber: position.lineNumber,
startColumn: position.column,
endLineNumber: textRange.endLineNumber,
endColumn: textRange.endColumn,
});
let wordRange: monaco.Range;
let aSuggestions;
if (context.triggerCharacter === '{') {
const wordUntil = model.getWordAtPosition(position.delta(0, -3));
if (wordUntil) {
wordRange = new monaco.Range(
position.lineNumber,
position.column,
position.lineNumber,
position.column
);
// Retrieve suggestions for subexpressions
// TODO: make this work for expressions nested more than one level deep
aSuggestions = getAutocompleteSuggestions(
expressionFunctions,
text.substring(0, text.length - lengthAfterPosition) + '}',
text.length - lengthAfterPosition
);
}
} else {
const wordUntil = model.getWordUntilPosition(position);
wordRange = new monaco.Range(
position.lineNumber,
wordUntil.startColumn,
position.lineNumber,
wordUntil.endColumn
);
aSuggestions = getAutocompleteSuggestions(
expressionFunctions,
text,
text.length - lengthAfterPosition
);
}
if (!aSuggestions) {
return { suggestions: [] };
}
const suggestions = aSuggestions.map((s: AutocompleteSuggestion, index) => {
const sortText = String.fromCharCode(index);
if (s.type === 'argument') {
return {
label: s.argDef.name,
kind: monaco.languages.CompletionItemKind.Variable,
documentation: { value: getArgReferenceStr(s.argDef), isTrusted: true },
insertText: s.text,
command: {
title: 'Trigger Suggestion Dialog',
id: 'editor.action.triggerSuggest',
},
range: wordRange,
sortText,
};
} else if (s.type === 'value') {
return {
label: s.text,
kind: monaco.languages.CompletionItemKind.Value,
insertText: s.text,
command: {
title: 'Trigger Suggestion Dialog',
id: 'editor.action.triggerSuggest',
},
range: wordRange,
sortText,
};
} else {
return {
label: s.fnDef.name,
kind: monaco.languages.CompletionItemKind.Function,
documentation: {
value: getFunctionReferenceStr(s.fnDef),
isTrusted: true,
},
insertText: s.text,
command: {
title: 'Trigger Suggestion Dialog',
id: 'editor.action.triggerSuggest',
},
range: wordRange,
sortText,
};
}
});
return {
suggestions,
};
};
return {
triggerCharacters: [' ', '{'],
provideCompletionItems,
};
};
export const getHoverProvider = (expressionFunctions: ExpressionFunction[]) => {
const provideHover = (model: monaco.editor.ITextModel, position: monaco.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(
expressionFunctions,
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 monaco.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: [],
};
};
return { provideHover };
};

View file

@ -1,21 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 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 { ExpressionFunction, ExpressionFunctionParameter } from 'src/plugins/expressions/common';
import { i18n } from '@kbn/i18n';
import {
ExpressionFunction,
ExpressionFunctionParameter,
} from '../../../../../../src/plugins/expressions';
import { BOLD_MD_TOKEN } from '../../../i18n/constants';
const BOLD_MD_TOKEN = '**';
const strings = {
getArgReferenceAliasesDetail: (aliases: string) =>
i18n.translate('xpack.canvas.expressionInput.argReferenceAliasesDetail', {
i18n.translate('presentationUtil.expressionInput.argReferenceAliasesDetail', {
defaultMessage: '{BOLD_MD_TOKEN}Aliases{BOLD_MD_TOKEN}: {aliases}',
values: {
BOLD_MD_TOKEN,
@ -23,7 +21,7 @@ const strings = {
},
}),
getArgReferenceDefaultDetail: (defaultVal: string) =>
i18n.translate('xpack.canvas.expressionInput.argReferenceDefaultDetail', {
i18n.translate('presentationUtil.expressionInput.argReferenceDefaultDetail', {
defaultMessage: '{BOLD_MD_TOKEN}Default{BOLD_MD_TOKEN}: {defaultVal}',
values: {
BOLD_MD_TOKEN,
@ -31,7 +29,7 @@ const strings = {
},
}),
getArgReferenceRequiredDetail: (required: string) =>
i18n.translate('xpack.canvas.expressionInput.argReferenceRequiredDetail', {
i18n.translate('presentationUtil.expressionInput.argReferenceRequiredDetail', {
defaultMessage: '{BOLD_MD_TOKEN}Required{BOLD_MD_TOKEN}: {required}',
values: {
BOLD_MD_TOKEN,
@ -39,7 +37,7 @@ const strings = {
},
}),
getArgReferenceTypesDetail: (types: string) =>
i18n.translate('xpack.canvas.expressionInput.argReferenceTypesDetail', {
i18n.translate('presentationUtil.expressionInput.argReferenceTypesDetail', {
defaultMessage: '{BOLD_MD_TOKEN}Types{BOLD_MD_TOKEN}: {types}',
values: {
BOLD_MD_TOKEN,
@ -47,7 +45,7 @@ const strings = {
},
}),
getFunctionReferenceAcceptsDetail: (acceptTypes: string) =>
i18n.translate('xpack.canvas.expressionInput.functionReferenceAccepts', {
i18n.translate('presentationUtil.expressionInput.functionReferenceAccepts', {
defaultMessage: '{BOLD_MD_TOKEN}Accepts{BOLD_MD_TOKEN}: {acceptTypes}',
values: {
BOLD_MD_TOKEN,
@ -55,7 +53,7 @@ const strings = {
},
}),
getFunctionReferenceReturnsDetail: (returnType: string) =>
i18n.translate('xpack.canvas.expressionInput.functionReferenceReturns', {
i18n.translate('presentationUtil.expressionInput.functionReferenceReturns', {
defaultMessage: '{BOLD_MD_TOKEN}Returns{BOLD_MD_TOKEN}: {returnType}',
values: {
BOLD_MD_TOKEN,

View file

@ -38,4 +38,9 @@ export const LazySavedObjectSaveModalDashboard = React.lazy(
() => import('./saved_object_save_modal_dashboard')
);
/**
* A lazily-loaded ExpressionInput component.
*/
export const LazyExpressionInput = React.lazy(() => import('./expression_input'));
export * from './types';

View file

@ -6,6 +6,12 @@
* Side Public License, v 1.
*/
import type { MutableRefObject } from 'react';
import type { monaco } from '@kbn/monaco';
import type { CSSProperties, HTMLAttributes } from 'react';
import type { ExpressionFunction } from '../../../expressions/common';
import { OnSaveProps, SaveModalState } from '../../../../plugins/saved_objects/public';
interface SaveModalDocumentInfo {
@ -22,3 +28,40 @@ export interface SaveModalDashboardProps {
onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void;
tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode);
}
/**
* A type for any React Ref that can be used to store a reference to the Monaco editor within the
* ExpressionInput.
*/
export type ExpressionInputEditorRef = MutableRefObject<monaco.editor.IStandaloneCodeEditor | null>;
/**
* React Props for the ExpressionInput component.
*/
export interface ExpressionInputProps
extends Pick<HTMLAttributes<HTMLDivElement>, 'style' | 'className'> {
/** A collection of ExpressionFunctions to use in the autocomplete */
expressionFunctions: ExpressionFunction[];
/** Value of expression */
expression: string;
/** Function invoked when expression value is changed */
onChange: (value?: string) => void;
/** In full screen mode or not */
isCompact?: boolean;
/**
* The CodeEditor requires a set height, either on itself, or set to 100% with the parent
* container controlling the height. This prop is required so consumers understand this
* limitation and are intentional in using the component.
*/
height: CSSProperties['height'];
/**
* An optional ref in order to access the Monaco editor instance from consuming components,
* (e.g. to determine if the editor is focused, etc).
*/
editorRef?: ExpressionInputEditorRef;
}

View file

@ -9,7 +9,9 @@
// TODO: https://github.com/elastic/kibana/issues/110893
/* eslint-disable @kbn/eslint/no_export_all */
import { ExpressionFunction } from 'src/plugins/expressions';
import { PresentationUtilPlugin } from './plugin';
import { pluginServices } from './services';
export type {
PresentationCapabilitiesService,
@ -33,6 +35,7 @@ export { projectIDs } from '../common/labs';
export * from '../common/lib';
export {
LazyExpressionInput,
LazyLabsBeakerButton,
LazyLabsFlyout,
LazyDashboardPicker,
@ -43,6 +46,7 @@ export {
export * from './components/types';
export type { QuickButtonProps } from './components/solution_toolbar';
export {
AddFromLibraryButton,
PrimaryActionButton,
@ -55,10 +59,21 @@ export {
export * from './components/controls';
/**
* Register a set of Expression Functions with the Presentation Utility ExpressionInput. This allows
* the Monaco Editor to understand the functions and their arguments.
*
* This function is async in order to move the logic to an async chunk.
*
* @param expressionFunctions A set of Expression Functions to use in the ExpressionInput.
*/
export const registerExpressionsLanguage = async (expressionFunctions: ExpressionFunction[]) => {
const languages = await import('./components/expression_input/language');
return languages.registerExpressionsLanguage(expressionFunctions);
};
export function plugin() {
return new PresentationUtilPlugin();
}
import { pluginServices } from './services';
export const useLabs = () => (() => pluginServices.getHooks().labs.useService())();

View file

@ -10,6 +10,7 @@ import { CoreStart } from 'kibana/public';
import { PresentationUtilPluginStart } from './types';
import { pluginServices } from './services';
import { registry } from './services/kibana';
import { registerExpressionsLanguage } from '.';
const createStartContract = (coreStart: CoreStart): PresentationUtilPluginStart => {
pluginServices.setRegistry(
@ -20,6 +21,7 @@ const createStartContract = (coreStart: CoreStart): PresentationUtilPluginStart
ContextProvider: pluginServices.getContextProvider(),
labsService: pluginServices.getServices().labs,
controlsService: pluginServices.getServices().controls,
registerExpressionsLanguage,
};
return startContract;
};

View file

@ -23,6 +23,8 @@ import {
import { OptionsListEmbeddableFactory } from './components/controls/control_types/options_list';
import { CONTROL_GROUP_TYPE, OPTIONS_LIST_CONTROL } from '.';
import { registerExpressionsLanguage } from '.';
export class PresentationUtilPlugin
implements
Plugin<
@ -93,6 +95,7 @@ export class PresentationUtilPlugin
ContextProvider: pluginServices.getContextProvider(),
controlsService,
labsService: pluginServices.getServices().labs,
registerExpressionsLanguage,
};
}

View file

@ -16,10 +16,12 @@ import { PresentationOverlaysService } from './overlays';
import { PresentationControlsService } from './controls';
import { PresentationDataViewsService } from './data_views';
import { PresentationDataService } from './data';
import { registerExpressionsLanguage } from '..';
export type { PresentationCapabilitiesService } from './capabilities';
export type { PresentationDashboardsService } from './dashboards';
export type { PresentationLabsService } from './labs';
export interface PresentationUtilServices {
dashboards: PresentationDashboardsService;
dataViews: PresentationDataViewsService;
@ -38,5 +40,6 @@ export const getStubPluginServices = (): PresentationUtilPluginStart => {
ContextProvider: pluginServices.getContextProvider(),
labsService: pluginServices.getServices().labs,
controlsService: pluginServices.getServices().controls,
registerExpressionsLanguage,
};
};

View file

@ -11,6 +11,7 @@ import { PresentationLabsService } from './services/labs';
import { PresentationControlsService } from './services/controls';
import { DataViewsPublicPluginStart } from '../../data_views/public';
import { EmbeddableSetup, EmbeddableStart } from '../../embeddable/public';
import { registerExpressionsLanguage } from '.';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PresentationUtilPluginSetup {}
@ -19,6 +20,7 @@ export interface PresentationUtilPluginStart {
ContextProvider: React.FC;
labsService: PresentationLabsService;
controlsService: PresentationControlsService;
registerExpressionsLanguage: typeof registerExpressionsLanguage;
}
export interface PresentationUtilPluginSetupDeps {

View file

@ -14,6 +14,22 @@ import { pluginServices } from '../public/services';
import { PresentationUtilServices } from '../public/services';
import { providers, StorybookParams } from '../public/services/storybook';
import { PluginServiceRegistry } from '../public/services/create';
import { KibanaContextProvider as KibanaReactProvider } from '../../kibana_react/public';
const settings = new Map();
settings.set('darkMode', true);
const services = {
http: {
basePath: {
get: () => '',
prepend: () => '',
remove: () => '',
serverBasePath: '',
},
},
uiSettings: settings,
};
export const servicesContextDecorator: DecoratorFn = (story: Function, storybook) => {
const registry = new PluginServiceRegistry<PresentationUtilServices, StorybookParams>(providers);
@ -22,7 +38,9 @@ export const servicesContextDecorator: DecoratorFn = (story: Function, storybook
return (
<I18nProvider>
<ContextProvider>{story()}</ContextProvider>
<KibanaReactProvider services={services}>
<ContextProvider>{story()}</ContextProvider>
</KibanaReactProvider>
</I18nProvider>
);
};

View file

@ -8,7 +8,9 @@
import { defaultConfigWebFinal } from '@kbn/storybook';
// We have to do this because the kbn/storybook preset overrides the manager entries,
// so we can't customize the theme.
module.exports = {
...defaultConfigWebFinal,
addons: ['@storybook/addon-essentials'],
addons: ['@storybook/addon-a11y', '@storybook/addon-essentials'],
};

View file

@ -10,6 +10,9 @@ import { addons } from '@storybook/addons';
import { create } from '@storybook/theming';
import { PANEL_ID } from '@storybook/addon-actions';
// @ts-expect-error There's probably a better way to do this.
import { registerThemeSwitcherAddon } from '@kbn/storybook/target_node/lib/register_theme_switcher_addon';
addons.setConfig({
theme: create({
base: 'light',
@ -19,3 +22,5 @@ addons.setConfig({
showPanel: true.valueOf,
selectedPanel: PANEL_ID,
});
registerThemeSwitcherAddon();

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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { functionSpecs } from '../../__fixtures__/function_specs';
import {
FunctionSuggestion,
getAutocompleteSuggestions,
getFnArgDefAtPosition,
} from './autocomplete';
describe('autocomplete', () => {
describe('getFnArgDefAtPosition', () => {
it('should return function definition for plot', () => {
const expression = 'plot ';
const def = getFnArgDefAtPosition(functionSpecs, expression, expression.length);
const plotFn = functionSpecs.find((spec: any) => spec.name === 'plot');
expect(def.fnDef).toBe(plotFn);
});
});
describe('getAutocompleteSuggestions', () => {
it('should suggest functions', () => {
const suggestions = getAutocompleteSuggestions(functionSpecs, '', 0);
expect(suggestions.length).toBe(functionSpecs.length);
expect(suggestions[0].start).toBe(0);
expect(suggestions[0].end).toBe(0);
});
it('should suggest arguments', () => {
const expression = 'plot ';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const plotFn = functionSpecs.find((spec: any) => spec.name === 'plot');
expect(suggestions.length).toBe(Object.keys(plotFn!.args).length);
expect(suggestions[0].start).toBe(expression.length);
expect(suggestions[0].end).toBe(expression.length);
});
it('should suggest values', () => {
const expression = 'shape shape=';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const shapeFn = functionSpecs.find((spec: any) => spec.name === 'shape');
expect(suggestions.length).toBe(shapeFn!.args.shape.options.length);
expect(suggestions[0].start).toBe(expression.length);
expect(suggestions[0].end).toBe(expression.length);
});
it('should suggest functions inside an expression', () => {
const expression = 'if {}';
const suggestions = getAutocompleteSuggestions(
functionSpecs,
expression,
expression.length - 1
);
expect(suggestions.length).toBe(functionSpecs.length);
expect(suggestions[0].start).toBe(expression.length - 1);
expect(suggestions[0].end).toBe(expression.length - 1);
});
it('should rank functions inside an expression by their return type first', () => {
const expression = 'plot defaultStyle={}';
const suggestions = getAutocompleteSuggestions(
functionSpecs,
expression,
expression.length - 1
) as FunctionSuggestion[];
expect(suggestions.length).toBe(functionSpecs.length);
expect(suggestions[0].fnDef.name).toBe('seriesStyle');
});
it('should rank functions inside an expression with matching return types and contexts before just return type', () => {
const expression = 'staticColumn "hello" | ply expression={}';
const suggestions = getAutocompleteSuggestions(
functionSpecs,
expression,
expression.length - 1
) as FunctionSuggestion[];
expect(suggestions.length).toBe(functionSpecs.length);
expect(suggestions[0].fnDef.type).toBe('datatable');
expect(suggestions[0].fnDef.inputTypes).toEqual(['datatable']);
const withReturnOnly = suggestions.findIndex(
(suggestion) =>
suggestion.fnDef.type === 'datatable' &&
suggestion.fnDef.inputTypes &&
!(suggestion.fnDef.inputTypes as string[]).includes('datatable')
);
const withNeither = suggestions.findIndex(
(suggestion) =>
suggestion.fnDef.type !== 'datatable' &&
(!suggestion.fnDef.inputTypes ||
!(suggestion.fnDef.inputTypes as string[]).includes('datatable'))
);
expect(suggestions[0].fnDef.type).toBe('datatable');
expect(suggestions[0].fnDef.inputTypes).toEqual(['datatable']);
expect(withReturnOnly).toBeLessThan(withNeither);
});
it('should suggest arguments inside an expression', () => {
const expression = 'if {lt }';
const suggestions = getAutocompleteSuggestions(
functionSpecs,
expression,
expression.length - 1
);
const ltFn = functionSpecs.find((spec: any) => spec.name === 'lt');
expect(suggestions.length).toBe(Object.keys(ltFn!.args).length);
expect(suggestions[0].start).toBe(expression.length - 1);
expect(suggestions[0].end).toBe(expression.length - 1);
});
it('should suggest values inside an expression', () => {
const expression = 'if {shape shape=}';
const suggestions = getAutocompleteSuggestions(
functionSpecs,
expression,
expression.length - 1
);
const shapeFn = functionSpecs.find((spec: any) => spec.name === 'shape');
expect(suggestions.length).toBe(shapeFn!.args.shape.options.length);
expect(suggestions[0].start).toBe(expression.length - 1);
expect(suggestions[0].end).toBe(expression.length - 1);
});
it('should suggest values inside quotes', () => {
const expression = 'shape shape="ar"';
const suggestions = getAutocompleteSuggestions(
functionSpecs,
expression,
expression.length - 1
);
const shapeFn = functionSpecs.find((spec: any) => spec.name === 'shape');
expect(suggestions.length).toBe(shapeFn!.args.shape.options.length);
expect(suggestions[0].start).toBe(expression.length - '"ar"'.length);
expect(suggestions[0].end).toBe(expression.length);
});
it('should prioritize functions that match the previous function type', () => {
const expression = 'plot | ';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const renderIndex = suggestions.findIndex((suggestion) => suggestion.text.includes('render'));
const anyIndex = suggestions.findIndex((suggestion) => suggestion.text.includes('any'));
expect(renderIndex).toBeLessThan(anyIndex);
});
it('should alphabetize functions', () => {
const expression = '';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const metricIndex = suggestions.findIndex((suggestion) => suggestion.text.includes('metric'));
const anyIndex = suggestions.findIndex((suggestion) => suggestion.text.includes('any'));
expect(anyIndex).toBeLessThan(metricIndex);
});
it('should prioritize unnamed arguments', () => {
const expression = 'case ';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const whenIndex = suggestions.findIndex((suggestion) => suggestion.text.includes('when'));
const thenIndex = suggestions.findIndex((suggestion) => suggestion.text.includes('then'));
expect(whenIndex).toBeLessThan(thenIndex);
});
it('should alphabetize arguments', () => {
const expression = 'plot ';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const yaxisIndex = suggestions.findIndex((suggestion) => suggestion.text.includes('yaxis'));
const defaultStyleIndex = suggestions.findIndex((suggestion) =>
suggestion.text.includes('defaultStyle')
);
expect(defaultStyleIndex).toBeLessThan(yaxisIndex);
});
it('should quote string values', () => {
const expression = 'shape shape=';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
expect(suggestions[0].text.trim()).toMatch(/^".*"$/);
});
it('should not quote sub expression value suggestions', () => {
const expression = 'plot font=';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
expect(suggestions[0].text.trim()).toBe('{font}');
});
it('should not quote booleans', () => {
const expression = 'table paginate=true';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
expect(suggestions[0].text.trim()).toBe('true');
});
});
});

View file

@ -6,7 +6,6 @@
*/
export * from './datatable';
export * from './autocomplete';
export * from './constants';
export * from './errors';
export * from './expression_form_handlers';

View file

@ -22,7 +22,6 @@ import { PluginServices } from '../../../../src/plugins/presentation_util/public
import { CanvasStartDeps, CanvasSetupDeps } from './plugin';
import { App } from './components/app';
import { registerLanguage } from './lib/monaco_language_def';
import { SetupRegistries } from './plugin_api';
import { initRegistries, populateRegistries, destroyRegistries } from './registries';
import { HelpMenu } from './components/help_menu/help_menu';
@ -121,8 +120,6 @@ export const initializeCanvas = async (
// Create Store
const canvasStore = await createStore(coreSetup);
registerLanguage(Object.values(expressions.getFunctions()));
// Init Registries
initRegistries();
await populateRegistries(registries);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { FC, MutableRefObject, useRef } from 'react';
import React, { FC, useRef } from 'react';
import PropTypes from 'prop-types';
import {
EuiPanel,
@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n';
// @ts-expect-error
import { Shortcuts } from 'react-shortcuts';
import { ExpressionInputEditorRef } from 'src/plugins/presentation_util/public';
import { ExpressionInput } from '../expression_input';
import { ToolTipShortcut } from '../tool_tip_shortcut';
import { ExpressionFunction } from '../../../types';
@ -58,15 +59,11 @@ const strings = {
}),
};
const shortcut = (
ref: MutableRefObject<ExpressionInput | null>,
cmd: string,
callback: () => void
) => (
const shortcut = (ref: ExpressionInputEditorRef, cmd: string, callback: () => void) => (
<Shortcuts
name="EXPRESSION"
handler={(command: string) => {
const isInputActive = ref.current && ref.current.editor && ref.current.editor.hasTextFocus();
const isInputActive = ref.current && ref.current && ref.current.hasTextFocus();
if (isInputActive && command === cmd) {
callback();
}
@ -98,7 +95,7 @@ export const Expression: FC<Props> = ({
isCompact,
toggleCompactView,
}) => {
const refExpressionInput = useRef<null | ExpressionInput>(null);
const refExpressionInput: ExpressionInputEditorRef = useRef(null);
const handleRun = () => {
setExpression(formState.expression);
@ -124,12 +121,12 @@ export const Expression: FC<Props> = ({
{/* Error code below is to pass a non breaking space so the editor does not jump */}
<ExpressionInput
ref={refExpressionInput}
isCompact={isCompact}
functionDefinitions={functionDefinitions}
expressionFunctions={functionDefinitions}
error={error ? error : `\u00A0`}
value={formState.expression}
expression={formState.expression}
onChange={updateValue}
editorRef={refExpressionInput}
/>
<div className="canvasExpression__settings">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">

View file

@ -110,11 +110,16 @@ const ExpressionContainer: FC<ExpressionContainerProps> = ({ done, element, page
}
}, [element, setFormState, formState]);
const functionDefinitions = useMemo(
() => Object.values(expressions.getFunctions()),
[expressions]
);
return (
<Component
done={done}
isCompact={isCompact}
functionDefinitions={Object.values(expressions.getFunctions())}
functionDefinitions={functionDefinitions}
formState={formState}
setExpression={onSetExpression}
toggleCompactView={toggleCompactView}

View file

@ -17,38 +17,7 @@ exports[`Storyshots components/ExpressionInput default 1`] = `
onBlur={[Function]}
onFocus={[Function]}
>
<div
className="kibanaCodeEditor"
>
<span
className="euiToolTipAnchor euiToolTipAnchor--displayBlock"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
aria-label="Code Editor"
className="kibanaCodeEditor__keyboardHint"
data-test-subj="codeEditorHint"
id="codeEditor_generated-id"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
/>
</span>
<div
className="react-monaco-editor-container"
style={
Object {
"height": "100%",
"width": "100%",
}
}
/>
<div />
</div>
ExpressionInput
</div>
</div>
</div>

View file

@ -8,10 +8,36 @@
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { monaco } from '@kbn/monaco';
import { ExpressionFunction, ExpressionFunctionParameter, Style } from 'src/plugins/expressions';
import { ExpressionInput } from '../expression_input';
import { language, LANGUAGE_ID } from '../../../lib/monaco_language_def';
import { registerExpressionsLanguage } from '../../../../../../../src/plugins/presentation_util/public';
const content: ExpressionFunctionParameter<'string'> = {
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: [],
accepts: () => true,
};
const font: ExpressionFunctionParameter<Style> = {
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: [],
accepts: () => true,
};
const sampleFunctionDef = {
name: 'markdown',
@ -19,48 +45,24 @@ const sampleFunctionDef = {
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'],
content,
font,
},
fn: () => {
return true;
},
};
fn: () => ({
as: 'markdown',
value: true,
type: 'render',
}),
} as unknown as ExpressionFunction;
language.keywords = [sampleFunctionDef.name];
monaco.languages.register({ id: LANGUAGE_ID });
monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, language);
registerExpressionsLanguage([sampleFunctionDef]);
storiesOf('components/ExpressionInput', module).add('default', () => (
<ExpressionInput
value="markdown"
expression="markdown"
isCompact={true}
onChange={action('onChange')}
functionDefinitions={[sampleFunctionDef as any]}
expressionFunctions={[sampleFunctionDef as any]}
/>
));

View file

@ -6,332 +6,34 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiFormRow } from '@elastic/eui';
import { debounce } from 'lodash';
import { monaco } from '@kbn/monaco';
import { ExpressionFunction } from '../../../types';
import { CodeEditor } from '../../../../../../src/plugins/kibana_react/public';
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: ExpressionFunction[];
LazyExpressionInput,
ExpressionInputProps,
withSuspense,
} from '../../../../../../src/plugins/presentation_util/public';
interface Props extends Omit<ExpressionInputProps, 'height'> {
/** 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,
const Input = withSuspense(LazyExpressionInput);
value: PropTypes.string.isRequired,
error: PropTypes.string,
onChange: PropTypes.func.isRequired,
};
editor: monaco.editor.IStandaloneCodeEditor | null;
undoHistory: string[];
redoHistory: string[];
constructor(props: Props) {
super(props);
this.undoHistory = [];
this.redoHistory = [];
this.editor = null;
}
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 }
export const ExpressionInput = ({ error, ...rest }: Props) => {
return (
<div className="canvasExpressionInput">
<EuiFormRow
className="canvasExpressionInput__inner"
fullWidth
isInvalid={Boolean(error)}
error={error}
>
<div className="canvasExpressionInput__editor">
<Input height="100%" {...rest} />
</div>
</EuiFormRow>
</div>
);
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: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.CompletionContext
) => {
const text = model.getValue();
const textRange = model.getFullModelRange();
const lengthAfterPosition = model.getValueLengthInRange({
startLineNumber: position.lineNumber,
startColumn: position.column,
endLineNumber: textRange.endLineNumber,
endColumn: textRange.endColumn,
});
let wordRange: monaco.Range;
let aSuggestions;
if (context.triggerCharacter === '{') {
const wordUntil = model.getWordAtPosition(position.delta(0, -3));
if (wordUntil) {
wordRange = new monaco.Range(
position.lineNumber,
position.column,
position.lineNumber,
position.column
);
// Retrieve suggestions for subexpressions
// TODO: make this work for expressions nested more than one level deep
aSuggestions = getAutocompleteSuggestions(
this.props.functionDefinitions,
text.substring(0, text.length - lengthAfterPosition) + '}',
text.length - lengthAfterPosition
);
}
} else {
const wordUntil = model.getWordUntilPosition(position);
wordRange = new monaco.Range(
position.lineNumber,
wordUntil.startColumn,
position.lineNumber,
wordUntil.endColumn
);
aSuggestions = getAutocompleteSuggestions(
this.props.functionDefinitions,
text,
text.length - lengthAfterPosition
);
}
if (!aSuggestions) {
return { suggestions: [] };
}
const suggestions = aSuggestions.map((s: AutocompleteSuggestion, index) => {
const sortText = String.fromCharCode(index);
if (s.type === 'argument') {
return {
label: s.argDef.name,
kind: monaco.languages.CompletionItemKind.Variable,
documentation: { value: getArgReferenceStr(s.argDef), isTrusted: true },
insertText: s.text,
command: {
title: 'Trigger Suggestion Dialog',
id: 'editor.action.triggerSuggest',
},
range: wordRange,
sortText,
};
} else if (s.type === 'value') {
return {
label: s.text,
kind: monaco.languages.CompletionItemKind.Value,
insertText: s.text,
command: {
title: 'Trigger Suggestion Dialog',
id: 'editor.action.triggerSuggest',
},
range: wordRange,
sortText,
};
} else {
return {
label: s.fnDef.name,
kind: monaco.languages.CompletionItemKind.Function,
documentation: {
value: getFunctionReferenceStr(s.fnDef),
isTrusted: true,
},
insertText: s.text,
command: {
title: 'Trigger Suggestion Dialog',
id: 'editor.action.triggerSuggest',
},
range: wordRange,
sortText,
};
}
});
return {
suggestions,
};
};
providerHover = (model: monaco.editor.ITextModel, position: monaco.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 monaco.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: [],
};
};
editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
// Updating tab size for the editor
const model = editor.getModel();
if (model) {
model.updateOptions({ tabSize: 2 });
}
this.editor = editor;
};
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">
<CodeEditor
languageId={LANGUAGE_ID}
languageConfiguration={{
autoClosingPairs: [
{
open: '{',
close: '}',
},
],
}}
value={value}
onChange={this.onChange}
suggestionProvider={{
triggerCharacters: [' ', '{'],
provideCompletionItems: this.provideSuggestions,
}}
hoverProvider={{
provideHover: this.providerHover,
}}
options={{
fontSize: isCompact ? 12 : 14,
scrollBeyondLastLine: false,
quickSuggestions: true,
minimap: {
enabled: false,
},
wordWrap: 'on',
wrappingIndent: 'indent',
}}
editorDidMount={this.editorDidMount}
/>
</div>
</EuiFormRow>
</div>
);
}
}
};

View file

@ -132,6 +132,11 @@ export class CanvasPlugin
})
);
const { expressions, presentationUtil } = startPlugins;
await presentationUtil.registerExpressionsLanguage(
Object.values(expressions.getFunctions())
);
// Load application bundle
const { renderApp, initializeCanvas, teardownCanvas } = await import('./application');

View file

@ -72,6 +72,11 @@ import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'
jest.mock('@elastic/eui/test-env/components/observer/observer');
EuiObserver.mockImplementation(() => 'EuiObserver');
import { ExpressionInput } from '../../../../src/plugins/presentation_util/public/components/expression_input';
jest.mock('../../../../src/plugins/presentation_util/public/components/expression_input');
// @ts-expect-error
ExpressionInput.mockImplementation(() => 'ExpressionInput');
// @ts-expect-error untyped library
import Dropzone from 'react-dropzone';
jest.mock('react-dropzone');

View file

@ -3580,6 +3580,12 @@
"newsfeed.headerButton.unreadAriaLabel": "ニュースフィードメニュー - 未読の項目があります",
"newsfeed.loadingPrompt.gettingNewsText": "最新ニュースを取得しています...",
"presentationUtil.dashboardPicker.searchDashboardPlaceholder": "ダッシュボードを検索...",
"presentationUtil.expressionInput.argReferenceAliasesDetail": "{BOLD_MD_TOKEN}エイリアス{BOLD_MD_TOKEN}: {aliases}",
"presentationUtil.expressionInput.argReferenceDefaultDetail": "{BOLD_MD_TOKEN}Default{BOLD_MD_TOKEN}: {defaultVal}",
"presentationUtil.expressionInput.argReferenceRequiredDetail": "{BOLD_MD_TOKEN}必須{BOLD_MD_TOKEN}{required}",
"presentationUtil.expressionInput.argReferenceTypesDetail": "{BOLD_MD_TOKEN}タイプ{BOLD_MD_TOKEN}: {types}",
"presentationUtil.expressionInput.functionReferenceAccepts": "{BOLD_MD_TOKEN}承諾{BOLD_MD_TOKEN}{acceptTypes}",
"presentationUtil.expressionInput.functionReferenceReturns": "{BOLD_MD_TOKEN}返す{BOLD_MD_TOKEN}{returnType}",
"presentationUtil.labs.components.browserSwitchHelp": "このブラウザーでラボを有効にします。ブラウザーを閉じた後も永続します。",
"presentationUtil.labs.components.browserSwitchName": "ブラウザー",
"presentationUtil.labs.components.calloutHelp": "変更を適用するには更新します",
@ -6649,12 +6655,6 @@
"xpack.canvas.expression.runTooltip": "表現を実行",
"xpack.canvas.expressionElementNotSelected.closeButtonLabel": "閉じる",
"xpack.canvas.expressionElementNotSelected.selectDescription": "表現インプットを表示するエレメントを選択します",
"xpack.canvas.expressionInput.argReferenceAliasesDetail": "{BOLD_MD_TOKEN}エイリアス{BOLD_MD_TOKEN}: {aliases}",
"xpack.canvas.expressionInput.argReferenceDefaultDetail": "{BOLD_MD_TOKEN}Default{BOLD_MD_TOKEN}: {defaultVal}",
"xpack.canvas.expressionInput.argReferenceRequiredDetail": "{BOLD_MD_TOKEN}必須{BOLD_MD_TOKEN}{required}",
"xpack.canvas.expressionInput.argReferenceTypesDetail": "{BOLD_MD_TOKEN}タイプ{BOLD_MD_TOKEN}: {types}",
"xpack.canvas.expressionInput.functionReferenceAccepts": "{BOLD_MD_TOKEN}承諾{BOLD_MD_TOKEN}{acceptTypes}",
"xpack.canvas.expressionInput.functionReferenceReturns": "{BOLD_MD_TOKEN}返す{BOLD_MD_TOKEN}{returnType}",
"xpack.canvas.expressionTypes.argTypes.colorDisplayName": "色",
"xpack.canvas.expressionTypes.argTypes.colorHelp": "カラーピッカー",
"xpack.canvas.expressionTypes.argTypes.containerStyle.appearanceTitle": "見た目",

View file

@ -3605,6 +3605,12 @@
"newsfeed.headerButton.unreadAriaLabel": "新闻源菜单 - 存在未读项目",
"newsfeed.loadingPrompt.gettingNewsText": "正在获取最近的新闻......",
"presentationUtil.dashboardPicker.searchDashboardPlaceholder": "搜索仪表板......",
"presentationUtil.expressionInput.argReferenceAliasesDetail": "{BOLD_MD_TOKEN}别名{BOLD_MD_TOKEN}{aliases}",
"presentationUtil.expressionInput.argReferenceDefaultDetail": "{BOLD_MD_TOKEN}默认{BOLD_MD_TOKEN}{defaultVal}",
"presentationUtil.expressionInput.argReferenceRequiredDetail": "{BOLD_MD_TOKEN}必需{BOLD_MD_TOKEN}{required}",
"presentationUtil.expressionInput.argReferenceTypesDetail": "{BOLD_MD_TOKEN}类型{BOLD_MD_TOKEN}{types}",
"presentationUtil.expressionInput.functionReferenceAccepts": "{BOLD_MD_TOKEN}接受{BOLD_MD_TOKEN}{acceptTypes}",
"presentationUtil.expressionInput.functionReferenceReturns": "{BOLD_MD_TOKEN}返回{BOLD_MD_TOKEN}{returnType}",
"presentationUtil.labs.components.browserSwitchHelp": "启用此浏览器的实验并在其关闭后继续保持。",
"presentationUtil.labs.components.browserSwitchName": "浏览器",
"presentationUtil.labs.components.calloutHelp": "刷新以应用更改",
@ -6694,12 +6700,6 @@
"xpack.canvas.expression.runTooltip": "运行表达式",
"xpack.canvas.expressionElementNotSelected.closeButtonLabel": "关闭",
"xpack.canvas.expressionElementNotSelected.selectDescription": "选择元素以显示表达式输入",
"xpack.canvas.expressionInput.argReferenceAliasesDetail": "{BOLD_MD_TOKEN}别名{BOLD_MD_TOKEN}{aliases}",
"xpack.canvas.expressionInput.argReferenceDefaultDetail": "{BOLD_MD_TOKEN}默认{BOLD_MD_TOKEN}{defaultVal}",
"xpack.canvas.expressionInput.argReferenceRequiredDetail": "{BOLD_MD_TOKEN}必需{BOLD_MD_TOKEN}{required}",
"xpack.canvas.expressionInput.argReferenceTypesDetail": "{BOLD_MD_TOKEN}类型{BOLD_MD_TOKEN}{types}",
"xpack.canvas.expressionInput.functionReferenceAccepts": "{BOLD_MD_TOKEN}接受{BOLD_MD_TOKEN}{acceptTypes}",
"xpack.canvas.expressionInput.functionReferenceReturns": "{BOLD_MD_TOKEN}返回{BOLD_MD_TOKEN}{returnType}",
"xpack.canvas.expressionTypes.argTypes.colorDisplayName": "颜色",
"xpack.canvas.expressionTypes.argTypes.colorHelp": "颜色选取器",
"xpack.canvas.expressionTypes.argTypes.containerStyle.appearanceTitle": "外观",