mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
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:
parent
36d2ab49f7
commit
97e3bbd891
32 changed files with 662 additions and 656 deletions
|
@ -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';
|
||||
|
|
|
@ -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": []
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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',
|
||||
};
|
|
@ -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 }}
|
||||
/>
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
|
@ -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 };
|
||||
};
|
|
@ -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,
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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())();
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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'],
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
export * from './datatable';
|
||||
export * from './autocomplete';
|
||||
export * from './constants';
|
||||
export * from './errors';
|
||||
export * from './expression_form_handlers';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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": "見た目",
|
||||
|
|
|
@ -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": "外观",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue