diff --git a/package.json b/package.json
index 6580dddf6a820..0abf6eff415b7 100644
--- a/package.json
+++ b/package.json
@@ -336,6 +336,7 @@
"@types/mustache": "^0.8.31",
"@types/node": "^10.12.27",
"@types/opn": "^5.1.0",
+ "@types/pegjs": "^0.10.1",
"@types/pngjs": "^3.3.2",
"@types/podium": "^1.0.0",
"@types/prop-types": "^15.5.3",
diff --git a/renovate.json5 b/renovate.json5
index a6ae6ad557a78..6f5b9a9b5d76c 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -500,6 +500,14 @@
'@types/opn',
],
},
+ {
+ groupSlug: 'pegjs',
+ groupName: 'pegjs related packages',
+ packageNames: [
+ 'pegjs',
+ '@types/pegjs',
+ ],
+ },
{
groupSlug: 'pngjs',
groupName: 'pngjs related packages',
diff --git a/src/legacy/core_plugins/timelion/common/types.ts b/src/legacy/core_plugins/timelion/common/types.ts
new file mode 100644
index 0000000000000..f7084948a14f7
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/common/types.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+type TimelionFunctionArgsTypes = 'seriesList' | 'number' | 'string' | 'boolean' | 'null';
+
+interface TimelionFunctionArgsSuggestion {
+ name: string;
+ help: string;
+}
+
+export interface TimelionFunctionArgs {
+ name: string;
+ help?: string;
+ multi?: boolean;
+ types: TimelionFunctionArgsTypes[];
+ suggestions?: TimelionFunctionArgsSuggestion[];
+}
+
+export interface ITimelionFunction {
+ aliases: string[];
+ args: TimelionFunctionArgs[];
+ name: string;
+ help: string;
+ chainable: boolean;
+ extended: boolean;
+ isAlias: boolean;
+ argsByName: {
+ [key: string]: TimelionFunctionArgs[];
+ };
+}
diff --git a/src/legacy/core_plugins/timelion/public/components/_index.scss b/src/legacy/core_plugins/timelion/public/components/_index.scss
new file mode 100644
index 0000000000000..f2458a367e176
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/components/_index.scss
@@ -0,0 +1 @@
+@import './timelion_expression_input';
diff --git a/src/legacy/core_plugins/timelion/public/components/_timelion_expression_input.scss b/src/legacy/core_plugins/timelion/public/components/_timelion_expression_input.scss
new file mode 100644
index 0000000000000..b1c0b5514ff7a
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/components/_timelion_expression_input.scss
@@ -0,0 +1,18 @@
+.timExpressionInput {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+ margin-top: $euiSize;
+}
+
+.timExpressionInput__editor {
+ height: 100%;
+ padding-top: $euiSizeS;
+}
+
+@include euiBreakpoint('xs', 's', 'm') {
+ .timExpressionInput__editor {
+ height: $euiSize * 15;
+ max-height: $euiSize * 15;
+ }
+}
diff --git a/src/legacy/core_plugins/timelion/public/components/index.ts b/src/legacy/core_plugins/timelion/public/components/index.ts
new file mode 100644
index 0000000000000..8d7d32a3ba262
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/components/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export * from './timelion_expression_input';
+export * from './timelion_interval';
diff --git a/src/legacy/core_plugins/timelion/public/components/timelion_expression_input.tsx b/src/legacy/core_plugins/timelion/public/components/timelion_expression_input.tsx
new file mode 100644
index 0000000000000..c695d09ca822b
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/components/timelion_expression_input.tsx
@@ -0,0 +1,146 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useEffect, useCallback, useRef, useMemo } from 'react';
+import { EuiFormLabel } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
+
+import { CodeEditor, useKibana } from '../../../../../plugins/kibana_react/public';
+import { suggest, getSuggestion } from './timelion_expression_input_helpers';
+import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types';
+import { getArgValueSuggestions } from '../services/arg_value_suggestions';
+
+const LANGUAGE_ID = 'timelion_expression';
+monacoEditor.languages.register({ id: LANGUAGE_ID });
+
+interface TimelionExpressionInputProps {
+ value: string;
+ setValue(value: string): void;
+}
+
+function TimelionExpressionInput({ value, setValue }: TimelionExpressionInputProps) {
+ const functionList = useRef([]);
+ const kibana = useKibana();
+ const argValueSuggestions = useMemo(getArgValueSuggestions, []);
+
+ const provideCompletionItems = useCallback(
+ async (model: monacoEditor.editor.ITextModel, position: monacoEditor.Position) => {
+ const text = model.getValue();
+ const wordUntil = model.getWordUntilPosition(position);
+ const wordRange = new monacoEditor.Range(
+ position.lineNumber,
+ wordUntil.startColumn,
+ position.lineNumber,
+ wordUntil.endColumn
+ );
+
+ const suggestions = await suggest(
+ text,
+ functionList.current,
+ // it's important to offset the cursor position on 1 point left
+ // because of PEG parser starts the line with 0, but monaco with 1
+ position.column - 1,
+ argValueSuggestions
+ );
+
+ return {
+ suggestions: suggestions
+ ? suggestions.list.map((s: ITimelionFunction | TimelionFunctionArgs) =>
+ getSuggestion(s, suggestions.type, wordRange)
+ )
+ : [],
+ };
+ },
+ [argValueSuggestions]
+ );
+
+ const provideHover = useCallback(
+ async (model: monacoEditor.editor.ITextModel, position: monacoEditor.Position) => {
+ const suggestions = await suggest(
+ model.getValue(),
+ functionList.current,
+ // it's important to offset the cursor position on 1 point left
+ // because of PEG parser starts the line with 0, but monaco with 1
+ position.column - 1,
+ argValueSuggestions
+ );
+
+ return {
+ contents: suggestions
+ ? suggestions.list.map((s: ITimelionFunction | TimelionFunctionArgs) => ({
+ value: s.help,
+ }))
+ : [],
+ };
+ },
+ [argValueSuggestions]
+ );
+
+ useEffect(() => {
+ if (kibana.services.http) {
+ kibana.services.http.get('../api/timelion/functions').then(data => {
+ functionList.current = data;
+ });
+ }
+ }, [kibana.services.http]);
+
+ return (
+
+ );
+}
+
+export { TimelionExpressionInput };
diff --git a/src/legacy/core_plugins/timelion/public/components/timelion_expression_input_helpers.ts b/src/legacy/core_plugins/timelion/public/components/timelion_expression_input_helpers.ts
new file mode 100644
index 0000000000000..fc90c276eeca2
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/components/timelion_expression_input_helpers.ts
@@ -0,0 +1,287 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { get, startsWith } from 'lodash';
+import PEG from 'pegjs';
+import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
+
+// @ts-ignore
+import grammar from 'raw-loader!../chain.peg';
+
+import { i18n } from '@kbn/i18n';
+import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types';
+import { ArgValueSuggestions, FunctionArg, Location } from '../services/arg_value_suggestions';
+
+const Parser = PEG.generate(grammar);
+
+export enum SUGGESTION_TYPE {
+ ARGUMENTS = 'arguments',
+ ARGUMENT_VALUE = 'argument_value',
+ FUNCTIONS = 'functions',
+}
+
+function inLocation(cursorPosition: number, location: Location) {
+ return cursorPosition >= location.min && cursorPosition <= location.max;
+}
+
+function getArgumentsHelp(
+ functionHelp: ITimelionFunction | undefined,
+ functionArgs: FunctionArg[] = []
+) {
+ if (!functionHelp) {
+ return [];
+ }
+
+ // Do not provide 'inputSeries' as argument suggestion for chainable functions
+ const argsHelp = functionHelp.chainable ? functionHelp.args.slice(1) : functionHelp.args.slice(0);
+
+ // ignore arguments that are already provided in function declaration
+ const functionArgNames = functionArgs.map(arg => arg.name);
+ return argsHelp.filter(arg => !functionArgNames.includes(arg.name));
+}
+
+async function extractSuggestionsFromParsedResult(
+ result: ReturnType,
+ cursorPosition: number,
+ functionList: ITimelionFunction[],
+ argValueSuggestions: ArgValueSuggestions
+) {
+ const activeFunc = result.functions.find(({ location }: { location: Location }) =>
+ inLocation(cursorPosition, location)
+ );
+
+ if (!activeFunc) {
+ return;
+ }
+
+ const functionHelp = functionList.find(({ name }) => name === activeFunc.function);
+
+ if (!functionHelp) {
+ return;
+ }
+
+ // return function suggestion when cursor is outside of parentheses
+ // location range includes '.', function name, and '('.
+ const openParen = activeFunc.location.min + activeFunc.function.length + 2;
+ if (cursorPosition < openParen) {
+ return { list: [functionHelp], type: SUGGESTION_TYPE.FUNCTIONS };
+ }
+
+ // return argument value suggestions when cursor is inside argument value
+ const activeArg = activeFunc.arguments.find((argument: FunctionArg) => {
+ return inLocation(cursorPosition, argument.location);
+ });
+ if (
+ activeArg &&
+ activeArg.type === 'namedArg' &&
+ inLocation(cursorPosition, activeArg.value.location)
+ ) {
+ const { function: functionName, arguments: functionArgs } = activeFunc;
+
+ const {
+ name: argName,
+ value: { text: partialInput },
+ } = activeArg;
+
+ let valueSuggestions;
+ if (argValueSuggestions.hasDynamicSuggestionsForArgument(functionName, argName)) {
+ valueSuggestions = await argValueSuggestions.getDynamicSuggestionsForArgument(
+ functionName,
+ argName,
+ functionArgs,
+ partialInput
+ );
+ } else {
+ const { suggestions: staticSuggestions } =
+ functionHelp.args.find(arg => arg.name === activeArg.name) || {};
+ valueSuggestions = argValueSuggestions.getStaticSuggestionsForInput(
+ partialInput,
+ staticSuggestions
+ );
+ }
+ return {
+ list: valueSuggestions,
+ type: SUGGESTION_TYPE.ARGUMENT_VALUE,
+ };
+ }
+
+ // return argument suggestions
+ const argsHelp = getArgumentsHelp(functionHelp, activeFunc.arguments);
+ const argumentSuggestions = argsHelp.filter(arg => {
+ if (get(activeArg, 'type') === 'namedArg') {
+ return startsWith(arg.name, activeArg.name);
+ } else if (activeArg) {
+ return startsWith(arg.name, activeArg.text);
+ }
+ return true;
+ });
+ return { list: argumentSuggestions, type: SUGGESTION_TYPE.ARGUMENTS };
+}
+
+export async function suggest(
+ expression: string,
+ functionList: ITimelionFunction[],
+ cursorPosition: number,
+ argValueSuggestions: ArgValueSuggestions
+) {
+ try {
+ const result = await Parser.parse(expression);
+
+ return await extractSuggestionsFromParsedResult(
+ result,
+ cursorPosition,
+ functionList,
+ argValueSuggestions
+ );
+ } catch (err) {
+ let message: any;
+ try {
+ // The grammar will throw an error containing a message if the expression is formatted
+ // correctly and is prepared to accept suggestions. If the expression is not formatted
+ // correctly the grammar will just throw a regular PEG SyntaxError, and this JSON.parse
+ // attempt will throw an error.
+ message = JSON.parse(err.message);
+ } catch (e) {
+ // The expression isn't correctly formatted, so JSON.parse threw an error.
+ return;
+ }
+
+ switch (message.type) {
+ case 'incompleteFunction': {
+ let list;
+ if (message.function) {
+ // The user has start typing a function name, so we'll filter the list down to only
+ // possible matches.
+ list = functionList.filter(func => startsWith(func.name, message.function));
+ } else {
+ // The user hasn't typed anything yet, so we'll just return the entire list.
+ list = functionList;
+ }
+ return { list, type: SUGGESTION_TYPE.FUNCTIONS };
+ }
+ case 'incompleteArgument': {
+ const { currentFunction: functionName, currentArgs: functionArgs } = message;
+ const functionHelp = functionList.find(func => func.name === functionName);
+ return {
+ list: getArgumentsHelp(functionHelp, functionArgs),
+ type: SUGGESTION_TYPE.ARGUMENTS,
+ };
+ }
+ case 'incompleteArgumentValue': {
+ const { name: argName, currentFunction: functionName, currentArgs: functionArgs } = message;
+ let valueSuggestions = [];
+ if (argValueSuggestions.hasDynamicSuggestionsForArgument(functionName, argName)) {
+ valueSuggestions = await argValueSuggestions.getDynamicSuggestionsForArgument(
+ functionName,
+ argName,
+ functionArgs
+ );
+ } else {
+ const functionHelp = functionList.find(func => func.name === functionName);
+ if (functionHelp) {
+ const argHelp = functionHelp.args.find(arg => arg.name === argName);
+ if (argHelp && argHelp.suggestions) {
+ valueSuggestions = argHelp.suggestions;
+ }
+ }
+ }
+ return {
+ list: valueSuggestions,
+ type: SUGGESTION_TYPE.ARGUMENT_VALUE,
+ };
+ }
+ }
+ }
+}
+
+export function getSuggestion(
+ suggestion: ITimelionFunction | TimelionFunctionArgs,
+ type: SUGGESTION_TYPE,
+ range: monacoEditor.Range
+): monacoEditor.languages.CompletionItem {
+ let kind: monacoEditor.languages.CompletionItemKind =
+ monacoEditor.languages.CompletionItemKind.Method;
+ let insertText: string = suggestion.name;
+ let insertTextRules: monacoEditor.languages.CompletionItem['insertTextRules'];
+ let detail: string = '';
+ let command: monacoEditor.languages.CompletionItem['command'];
+
+ switch (type) {
+ case SUGGESTION_TYPE.ARGUMENTS:
+ command = {
+ title: 'Trigger Suggestion Dialog',
+ id: 'editor.action.triggerSuggest',
+ };
+ kind = monacoEditor.languages.CompletionItemKind.Property;
+ insertText = `${insertText}=`;
+ detail = `${i18n.translate(
+ 'timelion.expressionSuggestions.argument.description.acceptsText',
+ {
+ defaultMessage: 'Accepts',
+ }
+ )}: ${(suggestion as TimelionFunctionArgs).types}`;
+
+ break;
+ case SUGGESTION_TYPE.FUNCTIONS:
+ command = {
+ title: 'Trigger Suggestion Dialog',
+ id: 'editor.action.triggerSuggest',
+ };
+ kind = monacoEditor.languages.CompletionItemKind.Function;
+ insertText = `${insertText}($0)`;
+ insertTextRules = monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet;
+ detail = `(${
+ (suggestion as ITimelionFunction).chainable
+ ? i18n.translate('timelion.expressionSuggestions.func.description.chainableHelpText', {
+ defaultMessage: 'Chainable',
+ })
+ : i18n.translate('timelion.expressionSuggestions.func.description.dataSourceHelpText', {
+ defaultMessage: 'Data source',
+ })
+ })`;
+
+ break;
+ case SUGGESTION_TYPE.ARGUMENT_VALUE:
+ const param = suggestion.name.split(':');
+
+ if (param.length === 1 || param[1]) {
+ insertText = `${param.length === 1 ? insertText : param[1]},`;
+ }
+
+ command = {
+ title: 'Trigger Suggestion Dialog',
+ id: 'editor.action.triggerSuggest',
+ };
+ kind = monacoEditor.languages.CompletionItemKind.Property;
+ detail = suggestion.help || '';
+
+ break;
+ }
+
+ return {
+ detail,
+ insertText,
+ insertTextRules,
+ kind,
+ label: suggestion.name,
+ documentation: suggestion.help,
+ command,
+ range,
+ };
+}
diff --git a/src/legacy/core_plugins/timelion/public/components/timelion_interval.tsx b/src/legacy/core_plugins/timelion/public/components/timelion_interval.tsx
new file mode 100644
index 0000000000000..6294e51e54788
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/components/timelion_interval.tsx
@@ -0,0 +1,144 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useMemo, useCallback } from 'react';
+import { EuiFormRow, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { useValidation } from 'ui/vis/editors/default/controls/agg_utils';
+import { isValidEsInterval } from '../../../../core_plugins/data/common';
+
+const intervalOptions = [
+ {
+ label: i18n.translate('timelion.vis.interval.auto', {
+ defaultMessage: 'Auto',
+ }),
+ value: 'auto',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.second', {
+ defaultMessage: '1 second',
+ }),
+ value: '1s',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.minute', {
+ defaultMessage: '1 minute',
+ }),
+ value: '1m',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.hour', {
+ defaultMessage: '1 hour',
+ }),
+ value: '1h',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.day', {
+ defaultMessage: '1 day',
+ }),
+ value: '1d',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.week', {
+ defaultMessage: '1 week',
+ }),
+ value: '1w',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.month', {
+ defaultMessage: '1 month',
+ }),
+ value: '1M',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.year', {
+ defaultMessage: '1 year',
+ }),
+ value: '1y',
+ },
+];
+
+interface TimelionIntervalProps {
+ value: string;
+ setValue(value: string): void;
+ setValidity(valid: boolean): void;
+}
+
+function TimelionInterval({ value, setValue, setValidity }: TimelionIntervalProps) {
+ const onCustomInterval = useCallback(
+ (customValue: string) => {
+ setValue(customValue.trim());
+ },
+ [setValue]
+ );
+
+ const onChange = useCallback(
+ (opts: Array>) => {
+ setValue((opts[0] && opts[0].value) || '');
+ },
+ [setValue]
+ );
+
+ const selectedOptions = useMemo(
+ () => [intervalOptions.find(op => op.value === value) || { label: value, value }],
+ [value]
+ );
+
+ const isValid = intervalOptions.some(int => int.value === value) || isValidEsInterval(value);
+
+ useValidation(setValidity, isValid);
+
+ return (
+
+
+
+ );
+}
+
+export { TimelionInterval };
diff --git a/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js b/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js
index b90f5932b5b09..231330b898edb 100644
--- a/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js
+++ b/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js
@@ -21,9 +21,15 @@ import expect from '@kbn/expect';
import PEG from 'pegjs';
import grammar from 'raw-loader!../../chain.peg';
import { SUGGESTION_TYPE, suggest } from '../timelion_expression_input_helpers';
-import { ArgValueSuggestionsProvider } from '../timelion_expression_suggestions/arg_value_suggestions';
+import { getArgValueSuggestions } from '../../services/arg_value_suggestions';
+import { setIndexPatterns, setSavedObjectsClient } from '../../services/plugin_services';
describe('Timelion expression suggestions', () => {
+ setIndexPatterns({});
+ setSavedObjectsClient({});
+
+ const argValueSuggestions = getArgValueSuggestions();
+
describe('getSuggestions', () => {
const func1 = {
name: 'func1',
@@ -44,11 +50,6 @@ describe('Timelion expression suggestions', () => {
};
const functionList = [func1, myFunc2];
let Parser;
- const privateStub = () => {
- return {};
- };
- const indexPatternsStub = {};
- const argValueSuggestions = ArgValueSuggestionsProvider(privateStub, indexPatternsStub); // eslint-disable-line new-cap
beforeEach(function() {
Parser = PEG.generate(grammar);
});
diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
index 137dd6b82046d..449c0489fea25 100644
--- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
+++ b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
@@ -52,11 +52,11 @@ import {
insertAtLocation,
} from './timelion_expression_input_helpers';
import { comboBoxKeyCodes } from '@elastic/eui';
-import { ArgValueSuggestionsProvider } from './timelion_expression_suggestions/arg_value_suggestions';
+import { getArgValueSuggestions } from '../services/arg_value_suggestions';
const Parser = PEG.generate(grammar);
-export function TimelionExpInput($http, $timeout, Private) {
+export function TimelionExpInput($http, $timeout) {
return {
restrict: 'E',
scope: {
@@ -68,7 +68,7 @@ export function TimelionExpInput($http, $timeout, Private) {
replace: true,
template: timelionExpressionInputTemplate,
link: function(scope, elem) {
- const argValueSuggestions = Private(ArgValueSuggestionsProvider);
+ const argValueSuggestions = getArgValueSuggestions();
const expressionInput = elem.find('[data-expression-input]');
const functionReference = {};
let suggestibleFunctionLocation = {};
diff --git a/src/legacy/core_plugins/timelion/public/index.scss b/src/legacy/core_plugins/timelion/public/index.scss
index f6123f4052156..7ccc6c300bc40 100644
--- a/src/legacy/core_plugins/timelion/public/index.scss
+++ b/src/legacy/core_plugins/timelion/public/index.scss
@@ -11,5 +11,6 @@
// timChart__legend-isLoading
@import './app';
+@import './components/index';
@import './directives/index';
@import './vis/index';
diff --git a/src/legacy/core_plugins/timelion/public/legacy.ts b/src/legacy/core_plugins/timelion/public/legacy.ts
index d989a68d40eeb..1cf6bb65cdc02 100644
--- a/src/legacy/core_plugins/timelion/public/legacy.ts
+++ b/src/legacy/core_plugins/timelion/public/legacy.ts
@@ -37,4 +37,4 @@ const setupPlugins: Readonly = {
const pluginInstance = plugin({} as PluginInitializerContext);
export const setup = pluginInstance.setup(npSetup.core, setupPlugins);
-export const start = pluginInstance.start(npStart.core);
+export const start = pluginInstance.start(npStart.core, npStart.plugins);
diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts
index 04b27c4020ce3..0bbda4bf3646f 100644
--- a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts
+++ b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts
@@ -35,6 +35,7 @@ const DEBOUNCE_DELAY = 50;
export function timechartFn(dependencies: TimelionVisualizationDependencies) {
const { $rootScope, $compile, uiSettings } = dependencies;
+
return function() {
return {
help: 'Draw a timeseries chart',
diff --git a/src/legacy/core_plugins/timelion/public/plugin.ts b/src/legacy/core_plugins/timelion/public/plugin.ts
index ba8c25c20abea..42f0ee3ad4725 100644
--- a/src/legacy/core_plugins/timelion/public/plugin.ts
+++ b/src/legacy/core_plugins/timelion/public/plugin.ts
@@ -26,12 +26,14 @@ import {
} from 'kibana/public';
import { Plugin as ExpressionsPlugin } from 'src/plugins/expressions/public';
import { DataPublicPluginSetup, TimefilterContract } from 'src/plugins/data/public';
+import { PluginsStart } from 'ui/new_platform/new_platform';
import { VisualizationsSetup } from '../../visualizations/public/np_ready/public';
import { getTimelionVisualizationConfig } from './timelion_vis_fn';
import { getTimelionVisualization } from './vis';
import { getTimeChart } from './panels/timechart/timechart';
import { Panel } from './panels/panel';
import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim';
+import { setIndexPatterns, setSavedObjectsClient } from './services/plugin_services';
/** @internal */
export interface TimelionVisualizationDependencies extends LegacyDependenciesPluginSetup {
@@ -85,12 +87,15 @@ export class TimelionPlugin implements Plugin, void> {
dependencies.timelionPanels.set(timeChartPanel.name, timeChartPanel);
}
- public start(core: CoreStart) {
+ public start(core: CoreStart, plugins: PluginsStart) {
const timelionUiEnabled = core.injectedMetadata.getInjectedVar('timelionUiEnabled');
if (timelionUiEnabled === false) {
core.chrome.navLinks.update('timelion', { hidden: true });
}
+
+ setIndexPatterns(plugins.data.indexPatterns);
+ setSavedObjectsClient(core.savedObjects.client);
}
public stop(): void {}
diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js b/src/legacy/core_plugins/timelion/public/services/arg_value_suggestions.ts
similarity index 72%
rename from src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js
rename to src/legacy/core_plugins/timelion/public/services/arg_value_suggestions.ts
index e698a69401a37..8d133de51f6d9 100644
--- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js
+++ b/src/legacy/core_plugins/timelion/public/services/arg_value_suggestions.ts
@@ -17,33 +17,51 @@
* under the License.
*/
-import _ from 'lodash';
-import { npStart } from 'ui/new_platform';
+import { get } from 'lodash';
+import { TimelionFunctionArgs } from '../../common/types';
+import { getIndexPatterns, getSavedObjectsClient } from './plugin_services';
-export function ArgValueSuggestionsProvider() {
- const { indexPatterns } = npStart.plugins.data;
- const { client: savedObjectsClient } = npStart.core.savedObjects;
+export interface Location {
+ min: number;
+ max: number;
+}
- async function getIndexPattern(functionArgs) {
- const indexPatternArg = functionArgs.find(argument => {
- return argument.name === 'index';
- });
+export interface FunctionArg {
+ function: string;
+ location: Location;
+ name: string;
+ text: string;
+ type: string;
+ value: {
+ location: Location;
+ text: string;
+ type: string;
+ value: string;
+ };
+}
+
+export function getArgValueSuggestions() {
+ const indexPatterns = getIndexPatterns();
+ const savedObjectsClient = getSavedObjectsClient();
+
+ async function getIndexPattern(functionArgs: FunctionArg[]) {
+ const indexPatternArg = functionArgs.find(({ name }) => name === 'index');
if (!indexPatternArg) {
// index argument not provided
return;
}
- const indexPatternTitle = _.get(indexPatternArg, 'value.text');
+ const indexPatternTitle = get(indexPatternArg, 'value.text');
- const resp = await savedObjectsClient.find({
+ const { savedObjects } = await savedObjectsClient.find({
type: 'index-pattern',
fields: ['title'],
search: `"${indexPatternTitle}"`,
- search_fields: ['title'],
+ searchFields: ['title'],
perPage: 10,
});
- const indexPatternSavedObject = resp.savedObjects.find(savedObject => {
- return savedObject.attributes.title === indexPatternTitle;
- });
+ const indexPatternSavedObject = savedObjects.find(
+ ({ attributes }) => attributes.title === indexPatternTitle
+ );
if (!indexPatternSavedObject) {
// index argument does not match an index pattern
return;
@@ -52,7 +70,7 @@ export function ArgValueSuggestionsProvider() {
return await indexPatterns.get(indexPatternSavedObject.id);
}
- function containsFieldName(partial, field) {
+ function containsFieldName(partial: string, field: { name: string }) {
if (!partial) {
return true;
}
@@ -63,13 +81,13 @@ export function ArgValueSuggestionsProvider() {
// Could not put with function definition since functions are defined on server
const customHandlers = {
es: {
- index: async function(partial) {
+ async index(partial: string) {
const search = partial ? `${partial}*` : '*';
const resp = await savedObjectsClient.find({
type: 'index-pattern',
fields: ['title', 'type'],
search: `${search}`,
- search_fields: ['title'],
+ searchFields: ['title'],
perPage: 25,
});
return resp.savedObjects
@@ -78,7 +96,7 @@ export function ArgValueSuggestionsProvider() {
return { name: savedObject.attributes.title };
});
},
- metric: async function(partial, functionArgs) {
+ async metric(partial: string, functionArgs: FunctionArg[]) {
if (!partial || !partial.includes(':')) {
return [
{ name: 'avg:' },
@@ -109,7 +127,7 @@ export function ArgValueSuggestionsProvider() {
return { name: `${valueSplit[0]}:${field.name}`, help: field.type };
});
},
- split: async function(partial, functionArgs) {
+ async split(partial: string, functionArgs: FunctionArg[]) {
const indexPattern = await getIndexPattern(functionArgs);
if (!indexPattern) {
return [];
@@ -127,7 +145,7 @@ export function ArgValueSuggestionsProvider() {
return { name: field.name, help: field.type };
});
},
- timefield: async function(partial, functionArgs) {
+ async timefield(partial: string, functionArgs: FunctionArg[]) {
const indexPattern = await getIndexPattern(functionArgs);
if (!indexPattern) {
return [];
@@ -150,7 +168,10 @@ export function ArgValueSuggestionsProvider() {
* @param {string} argName - user provided argument name
* @return {boolean} true when dynamic suggestion handler provided for function argument
*/
- hasDynamicSuggestionsForArgument: (functionName, argName) => {
+ hasDynamicSuggestionsForArgument: (
+ functionName: T,
+ argName: keyof typeof customHandlers[T]
+ ) => {
return customHandlers[functionName] && customHandlers[functionName][argName];
},
@@ -161,12 +182,13 @@ export function ArgValueSuggestionsProvider() {
* @param {string} partial - user provided argument value
* @return {array} array of dynamic suggestions matching partial
*/
- getDynamicSuggestionsForArgument: async (
- functionName,
- argName,
- functionArgs,
+ getDynamicSuggestionsForArgument: async (
+ functionName: T,
+ argName: keyof typeof customHandlers[T],
+ functionArgs: FunctionArg[],
partialInput = ''
) => {
+ // @ts-ignore
return await customHandlers[functionName][argName](partialInput, functionArgs);
},
@@ -175,7 +197,10 @@ export function ArgValueSuggestionsProvider() {
* @param {array} staticSuggestions - argument value suggestions
* @return {array} array of static suggestions matching partial
*/
- getStaticSuggestionsForInput: (partialInput = '', staticSuggestions = []) => {
+ getStaticSuggestionsForInput: (
+ partialInput = '',
+ staticSuggestions: TimelionFunctionArgs['suggestions'] = []
+ ) => {
if (partialInput) {
return staticSuggestions.filter(suggestion => {
return suggestion.name.includes(partialInput);
@@ -186,3 +211,5 @@ export function ArgValueSuggestionsProvider() {
},
};
}
+
+export type ArgValueSuggestions = ReturnType;
diff --git a/src/legacy/core_plugins/timelion/public/services/plugin_services.ts b/src/legacy/core_plugins/timelion/public/services/plugin_services.ts
new file mode 100644
index 0000000000000..5ba4ee5e47983
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/services/plugin_services.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { IndexPatternsContract } from 'src/plugins/data/public';
+import { SavedObjectsClientContract } from 'kibana/public';
+import { createGetterSetter } from '../../../../../plugins/kibana_utils/public';
+
+export const [getIndexPatterns, setIndexPatterns] = createGetterSetter(
+ 'IndexPatterns'
+);
+
+export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter<
+ SavedObjectsClientContract
+>('SavedObjectsClient');
diff --git a/src/legacy/core_plugins/timelion/public/timelion_vis_fn.ts b/src/legacy/core_plugins/timelion/public/timelion_vis_fn.ts
index 474f464a550cd..206f9f5d8368d 100644
--- a/src/legacy/core_plugins/timelion/public/timelion_vis_fn.ts
+++ b/src/legacy/core_plugins/timelion/public/timelion_vis_fn.ts
@@ -28,7 +28,7 @@ const name = 'timelion_vis';
interface Arguments {
expression: string;
- interval: any;
+ interval: string;
}
interface RenderValue {
@@ -38,7 +38,7 @@ interface RenderValue {
}
type Context = KibanaContext | null;
-type VisParams = Arguments;
+export type VisParams = Arguments;
type Return = Promise>;
export const getTimelionVisualizationConfig = (
@@ -60,7 +60,7 @@ export const getTimelionVisualizationConfig = (
help: '',
},
interval: {
- types: ['string', 'null'],
+ types: ['string'],
default: 'auto',
help: '',
},
diff --git a/src/legacy/core_plugins/timelion/public/vis/_index.scss b/src/legacy/core_plugins/timelion/public/vis/_index.scss
index e44b6336d33c1..17a2018f7a56a 100644
--- a/src/legacy/core_plugins/timelion/public/vis/_index.scss
+++ b/src/legacy/core_plugins/timelion/public/vis/_index.scss
@@ -1 +1,2 @@
@import './timelion_vis';
+@import './timelion_editor';
diff --git a/src/legacy/core_plugins/timelion/public/vis/_timelion_editor.scss b/src/legacy/core_plugins/timelion/public/vis/_timelion_editor.scss
new file mode 100644
index 0000000000000..a9331930a86ff
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/vis/_timelion_editor.scss
@@ -0,0 +1,15 @@
+.visEditor--timelion {
+ vis-options-react-wrapper,
+ .visEditorSidebar__options,
+ .visEditorSidebar__timelionOptions {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .visEditor__sidebar {
+ @include euiBreakpoint('xs', 's', 'm') {
+ width: 100%;
+ }
+ }
+}
diff --git a/src/legacy/core_plugins/timelion/public/vis/index.ts b/src/legacy/core_plugins/timelion/public/vis/index.tsx
similarity index 80%
rename from src/legacy/core_plugins/timelion/public/vis/index.ts
rename to src/legacy/core_plugins/timelion/public/vis/index.tsx
index 7b82553a24e5b..1edcb0a5ce71c 100644
--- a/src/legacy/core_plugins/timelion/public/vis/index.ts
+++ b/src/legacy/core_plugins/timelion/public/vis/index.tsx
@@ -17,19 +17,24 @@
* under the License.
*/
+import React from 'react';
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { DefaultEditorSize } from 'ui/vis/editor_size';
+import { VisOptionsProps } from 'ui/vis/editors/default';
+import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public';
import { getTimelionRequestHandler } from './timelion_request_handler';
import visConfigTemplate from './timelion_vis.html';
-import editorConfigTemplate from './timelion_vis_params.html';
import { TimelionVisualizationDependencies } from '../plugin';
// @ts-ignore
import { AngularVisController } from '../../../../ui/public/vis/vis_types/angular_vis_type';
+import { TimelionOptions } from './timelion_options';
+import { VisParams } from '../timelion_vis_fn';
export const TIMELION_VIS_NAME = 'timelion';
export function getTimelionVisualization(dependencies: TimelionVisualizationDependencies) {
+ const { http, uiSettings } = dependencies;
const timelionRequestHandler = getTimelionRequestHandler(dependencies);
// return the visType object, which kibana will use to display and configure new
@@ -50,7 +55,11 @@ export function getTimelionVisualization(dependencies: TimelionVisualizationDepe
template: visConfigTemplate,
},
editorConfig: {
- optionsTemplate: editorConfigTemplate,
+ optionsTemplate: (props: VisOptionsProps) => (
+
+
+
+ ),
defaultSize: DefaultEditorSize.MEDIUM,
},
requestHandler: timelionRequestHandler,
diff --git a/src/legacy/core_plugins/timelion/public/vis/timelion_options.tsx b/src/legacy/core_plugins/timelion/public/vis/timelion_options.tsx
new file mode 100644
index 0000000000000..527fcc3bc6ce8
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/vis/timelion_options.tsx
@@ -0,0 +1,48 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useCallback } from 'react';
+import { EuiPanel } from '@elastic/eui';
+
+import { VisOptionsProps } from 'ui/vis/editors/default';
+import { VisParams } from '../timelion_vis_fn';
+import { TimelionInterval, TimelionExpressionInput } from '../components';
+
+function TimelionOptions({ stateParams, setValue, setValidity }: VisOptionsProps) {
+ const setInterval = useCallback((value: VisParams['interval']) => setValue('interval', value), [
+ setValue,
+ ]);
+ const setExpressionInput = useCallback(
+ (value: VisParams['expression']) => setValue('expression', value),
+ [setValue]
+ );
+
+ return (
+
+
+
+
+ );
+}
+
+export { TimelionOptions };
diff --git a/src/legacy/core_plugins/timelion/public/vis/timelion_vis_params.html b/src/legacy/core_plugins/timelion/public/vis/timelion_vis_params.html
deleted file mode 100644
index 9f2d2094fb1f7..0000000000000
--- a/src/legacy/core_plugins/timelion/public/vis/timelion_vis_params.html
+++ /dev/null
@@ -1,27 +0,0 @@
-
diff --git a/src/legacy/core_plugins/timelion/server/lib/classes/timelion_function.d.ts b/src/legacy/core_plugins/timelion/server/lib/classes/timelion_function.d.ts
index 6e32a4454e707..798902aa133de 100644
--- a/src/legacy/core_plugins/timelion/server/lib/classes/timelion_function.d.ts
+++ b/src/legacy/core_plugins/timelion/server/lib/classes/timelion_function.d.ts
@@ -17,6 +17,8 @@
* under the License.
*/
+import { TimelionFunctionArgs } from '../../../common/types';
+
export interface TimelionFunctionInterface extends TimelionFunctionConfig {
chainable: boolean;
originalFn: Function;
@@ -32,21 +34,6 @@ export interface TimelionFunctionConfig {
args: TimelionFunctionArgs[];
}
-export interface TimelionFunctionArgs {
- name: string;
- help?: string;
- multi?: boolean;
- types: TimelionFunctionArgsTypes[];
- suggestions?: TimelionFunctionArgsSuggestion[];
-}
-
-export type TimelionFunctionArgsTypes = 'seriesList' | 'number' | 'string' | 'boolean' | 'null';
-
-export interface TimelionFunctionArgsSuggestion {
- name: string;
- help: string;
-}
-
// eslint-disable-next-line import/no-default-export
export default class TimelionFunction {
constructor(name: string, config: TimelionFunctionConfig);
diff --git a/src/legacy/core_plugins/timelion/server/types.ts b/src/legacy/core_plugins/timelion/server/types.ts
index e612bc14a0daa..a035d64f764f1 100644
--- a/src/legacy/core_plugins/timelion/server/types.ts
+++ b/src/legacy/core_plugins/timelion/server/types.ts
@@ -17,12 +17,5 @@
* under the License.
*/
-export {
- TimelionFunctionInterface,
- TimelionFunctionConfig,
- TimelionFunctionArgs,
- TimelionFunctionArgsSuggestion,
- TimelionFunctionArgsTypes,
-} from './lib/classes/timelion_function';
-
+export { TimelionFunctionInterface, TimelionFunctionConfig } from './lib/classes/timelion_function';
export { TimelionRequestQuery } from './routes/run';
diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx
index 0ae77995c0502..62440f12c6d84 100644
--- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx
+++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx
@@ -78,6 +78,13 @@ export interface Props {
*/
hoverProvider?: monacoEditor.languages.HoverProvider;
+ /**
+ * Language config provider for bracket
+ * Documentation for the provider can be found here:
+ * https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.languageconfiguration.html
+ */
+ languageConfiguration?: monacoEditor.languages.LanguageConfiguration;
+
/**
* Function called before the editor is mounted in the view
*/
@@ -130,6 +137,13 @@ export class CodeEditor extends React.Component {
if (this.props.hoverProvider) {
monaco.languages.registerHoverProvider(this.props.languageId, this.props.hoverProvider);
}
+
+ if (this.props.languageConfiguration) {
+ monaco.languages.setLanguageConfiguration(
+ this.props.languageId,
+ this.props.languageConfiguration
+ );
+ }
});
// Register the theme
diff --git a/yarn.lock b/yarn.lock
index 28f875fd94b06..ddcad39c8d6cc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4434,6 +4434,11 @@
resolved "https://registry.yarnpkg.com/@types/parse-link-header/-/parse-link-header-1.0.0.tgz#69f059e40a0fa93dc2e095d4142395ae6adc5d7a"
integrity sha512-fCA3btjE7QFeRLfcD0Sjg+6/CnmC66HpMBoRfRzd2raTaWMJV21CCZ0LO8MOqf8onl5n0EPfjq4zDhbyX8SVwA==
+"@types/pegjs@^0.10.1":
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/@types/pegjs/-/pegjs-0.10.1.tgz#9a2f3961dc62430fdb21061eb0ddbd890f9e3b94"
+ integrity sha512-ra8IchO9odGQmYKbm+94K58UyKCEKdZh9y0vxhG4pIpOJOBlC1C+ZtBVr6jLs+/oJ4pl+1p/4t3JtBA8J10Vvw==
+
"@types/pngjs@^3.3.2":
version "3.3.2"
resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.3.2.tgz#8ed3bd655ab3a92ea32ada7a21f618e63b93b1d4"