From bd87d98fbdeabe93ac5a2a020a20cf68b36b938f Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 10 May 2021 10:29:44 +0200
Subject: [PATCH 1/3] refactoring
---
.../config_panel/config_panel.tsx | 3 +-
.../dimension_panel/dimension_editor.tsx | 2 +-
.../formula/{ => editor}/formula.scss | 0
.../formula/editor/formula_editor.tsx | 547 +++++++++++
.../formula/editor/formula_help.tsx | 175 ++++
.../definitions/formula/editor/index.ts | 8 +
.../{ => editor}/math_completion.test.ts | 0
.../formula/{ => editor}/math_completion.ts | 8 +-
.../{ => editor}/math_tokenization.tsx | 0
.../definitions/formula/formula.tsx | 907 +-----------------
.../definitions/formula/generate.ts | 90 ++
.../operations/definitions/formula/parse.ts | 150 +++
.../operations/definitions/formula/util.ts | 69 +-
.../definitions/formula/validation.ts | 23 +-
.../operations/layer_helpers.ts | 30 +-
15 files changed, 1031 insertions(+), 981 deletions(-)
rename x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/{ => editor}/formula.scss (100%)
create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx
create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/index.ts
rename x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/{ => editor}/math_completion.test.ts (100%)
rename x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/{ => editor}/math_completion.ts (98%)
rename x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/{ => editor}/math_tokenization.tsx (100%)
create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts
create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx
index 0c26530b6172c..79c7882a8d56e 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx
@@ -63,7 +63,8 @@ export function LayerPanels(
() => (datasourceId: string, newState: unknown) => {
dispatch({
type: 'UPDATE_DATASOURCE_STATE',
- updater: () => newState,
+ updater: (prevState: unknown) =>
+ typeof newState === 'function' ? newState(prevState) : newState,
datasourceId,
clearStagedPreview: false,
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
index 28a5438b7af08..c85bc9188f083 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
@@ -144,7 +144,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
};
const setIsCloseable = (isCloseable: boolean) => {
- setState({ ...state, isDimensionClosePrevented: !isCloseable });
+ setState((prevState) => ({ ...prevState, isDimensionClosePrevented: !isCloseable }));
};
const selectedOperationDefinition =
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.scss b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss
similarity index 100%
rename from x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.scss
rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
new file mode 100644
index 0000000000000..7b96aec4194a4
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
@@ -0,0 +1,547 @@
+/*
+ * 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 React, { useCallback, useEffect, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPopover } from '@elastic/eui';
+import { monaco } from '@kbn/monaco';
+import classNames from 'classnames';
+import { CodeEditor } from '../../../../../../../../../src/plugins/kibana_react/public';
+import type { CodeEditorProps } from '../../../../../../../../../src/plugins/kibana_react/public';
+import { ParamEditorProps } from '../../index';
+import { getManagedColumnsFrom } from '../../../layer_helpers';
+import { ErrorWrapper, runASTValidation, tryToParse } from '../validation';
+import { useDebounceWithOptions } from '../../helpers';
+import {
+ LensMathSuggestion,
+ SUGGESTION_TYPE,
+ suggest,
+ getSuggestion,
+ getSignatureHelp,
+ getHover,
+ getTokenInfo,
+ offsetToRowColumn,
+ monacoPositionToOffset,
+} from './math_completion';
+import { LANGUAGE_ID } from './math_tokenization';
+import { MemoizedFormulaHelp } from './formula_help';
+
+import './formula.scss';
+import { FormulaIndexPatternColumn, regenerateLayerFromAst } from '../formula';
+
+export function FormulaEditor({
+ layer,
+ updateLayer,
+ currentColumn,
+ columnId,
+ indexPattern,
+ operationDefinitionMap,
+ data,
+ toggleFullscreen,
+ isFullscreen,
+ setIsCloseable,
+}: ParamEditorProps) {
+ const [text, setText] = useState(currentColumn.params.formula);
+ const [isHelpOpen, setIsHelpOpen] = useState(false);
+ const editorModel = React.useRef(
+ monaco.editor.createModel(text ?? '', LANGUAGE_ID)
+ );
+ const overflowDiv1 = React.useRef();
+ const disposables = React.useRef([]);
+ const editor1 = React.useRef();
+
+ // The Monaco editor needs to have the overflowDiv in the first render. Using an effect
+ // requires a second render to work, so we are using an if statement to guarantee it happens
+ // on first render
+ if (!overflowDiv1?.current) {
+ const node1 = (overflowDiv1.current = document.createElement('div'));
+ node1.setAttribute('data-test-subj', 'lnsFormulaWidget');
+ // Monaco CSS is targeted on the monaco-editor class
+ node1.classList.add('lnsFormulaOverflow', 'monaco-editor');
+ document.body.appendChild(node1);
+ }
+
+ // Clean up the monaco editor and DOM on unmount
+ useEffect(() => {
+ const model = editorModel.current;
+ const allDisposables = disposables.current;
+ const editor1ref = editor1.current;
+ return () => {
+ model.dispose();
+ overflowDiv1.current?.parentNode?.removeChild(overflowDiv1.current);
+ editor1ref?.dispose();
+ allDisposables?.forEach((d) => d.dispose());
+ };
+ }, []);
+
+ useDebounceWithOptions(
+ () => {
+ if (!editorModel.current) return;
+
+ if (!text) {
+ monaco.editor.setModelMarkers(editorModel.current, 'LENS', []);
+ if (currentColumn.params.formula) {
+ // Only submit if valid
+ const { newLayer } = regenerateLayerFromAst(
+ text || '',
+ layer,
+ columnId,
+ currentColumn,
+ indexPattern,
+ operationDefinitionMap
+ );
+ updateLayer(newLayer);
+ }
+
+ return;
+ }
+
+ let errors: ErrorWrapper[] = [];
+
+ const { root, error } = tryToParse(text);
+ if (error) {
+ errors = [error];
+ } else if (root) {
+ const validationErrors = runASTValidation(
+ root,
+ layer,
+ indexPattern,
+ operationDefinitionMap
+ );
+ if (validationErrors.length) {
+ errors = validationErrors;
+ }
+ }
+
+ if (errors.length) {
+ monaco.editor.setModelMarkers(
+ editorModel.current,
+ 'LENS',
+ errors.flatMap((innerError) => {
+ if (innerError.locations.length) {
+ return innerError.locations.map((location) => {
+ const startPosition = offsetToRowColumn(text, location.min);
+ const endPosition = offsetToRowColumn(text, location.max);
+ return {
+ message: innerError.message,
+ startColumn: startPosition.column + 1,
+ startLineNumber: startPosition.lineNumber,
+ endColumn: endPosition.column + 1,
+ endLineNumber: endPosition.lineNumber,
+ severity:
+ innerError.severity === 'warning'
+ ? monaco.MarkerSeverity.Warning
+ : monaco.MarkerSeverity.Error,
+ };
+ });
+ } else {
+ // Parse errors return no location info
+ const startPosition = offsetToRowColumn(text, 0);
+ const endPosition = offsetToRowColumn(text, text.length - 1);
+ return [
+ {
+ message: innerError.message,
+ startColumn: startPosition.column + 1,
+ startLineNumber: startPosition.lineNumber,
+ endColumn: endPosition.column + 1,
+ endLineNumber: endPosition.lineNumber,
+ severity:
+ innerError.severity === 'warning'
+ ? monaco.MarkerSeverity.Warning
+ : monaco.MarkerSeverity.Error,
+ },
+ ];
+ }
+ })
+ );
+ } else {
+ monaco.editor.setModelMarkers(editorModel.current, 'LENS', []);
+
+ // Only submit if valid
+ const { newLayer, locations } = regenerateLayerFromAst(
+ text || '',
+ layer,
+ columnId,
+ currentColumn,
+ indexPattern,
+ operationDefinitionMap
+ );
+ updateLayer(newLayer);
+
+ const managedColumns = getManagedColumnsFrom(columnId, newLayer.columns);
+ const markers: monaco.editor.IMarkerData[] = managedColumns
+ .flatMap(([id, column]) => {
+ if (locations[id]) {
+ const def = operationDefinitionMap[column.operationType];
+ if (def.getErrorMessage) {
+ const messages = def.getErrorMessage(
+ newLayer,
+ id,
+ indexPattern,
+ operationDefinitionMap
+ );
+ if (messages) {
+ const startPosition = offsetToRowColumn(text, locations[id].min);
+ const endPosition = offsetToRowColumn(text, locations[id].max);
+ return [
+ {
+ message: messages.join(', '),
+ startColumn: startPosition.column + 1,
+ startLineNumber: startPosition.lineNumber,
+ endColumn: endPosition.column + 1,
+ endLineNumber: endPosition.lineNumber,
+ severity: monaco.MarkerSeverity.Warning,
+ },
+ ];
+ }
+ }
+ }
+ return [];
+ })
+ .filter((marker) => marker);
+ monaco.editor.setModelMarkers(editorModel.current, 'LENS', markers);
+ }
+ },
+ // Make it validate on flyout open in case of a broken formula left over
+ // from a previous edit
+ { skipFirstRender: text == null },
+ 256,
+ [text]
+ );
+
+ /**
+ * The way that Monaco requests autocompletion is not intuitive, but the way we use it
+ * we fetch new suggestions in these scenarios:
+ *
+ * - If the user types one of the trigger characters, suggestions are always fetched
+ * - When the user selects the kql= suggestion, we tell Monaco to trigger new suggestions after
+ * - When the user types the first character into an empty text box, Monaco requests suggestions
+ *
+ * Monaco also triggers suggestions automatically when there are no suggestions being displayed
+ * and the user types a non-whitespace character.
+ *
+ * While suggestions are being displayed, Monaco uses an in-memory cache of the last known suggestions.
+ */
+ const provideCompletionItems = useCallback(
+ async (
+ model: monaco.editor.ITextModel,
+ position: monaco.Position,
+ context: monaco.languages.CompletionContext
+ ) => {
+ const innerText = model.getValue();
+ const textRange = model.getFullModelRange();
+ let wordRange: monaco.Range;
+ let aSuggestions: { list: LensMathSuggestion[]; type: SUGGESTION_TYPE } = {
+ list: [],
+ type: SUGGESTION_TYPE.FIELD,
+ };
+
+ const lengthAfterPosition = model.getValueLengthInRange({
+ startLineNumber: position.lineNumber,
+ startColumn: position.column,
+ endLineNumber: textRange.endLineNumber,
+ endColumn: textRange.endColumn,
+ });
+
+ 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 = await suggest({
+ expression: innerText.substring(0, innerText.length - lengthAfterPosition) + ')',
+ position: innerText.length - lengthAfterPosition,
+ context,
+ indexPattern,
+ operationDefinitionMap,
+ data,
+ });
+ }
+ } else {
+ aSuggestions = await suggest({
+ expression: innerText,
+ position: innerText.length - lengthAfterPosition,
+ context,
+ indexPattern,
+ operationDefinitionMap,
+ data,
+ });
+ }
+
+ return {
+ suggestions: aSuggestions.list.map((s) =>
+ getSuggestion(s, aSuggestions.type, wordRange, operationDefinitionMap)
+ ),
+ };
+ },
+ [indexPattern, operationDefinitionMap, data]
+ );
+
+ const provideSignatureHelp = useCallback(
+ async (
+ model: monaco.editor.ITextModel,
+ position: monaco.Position,
+ token: monaco.CancellationToken,
+ context: monaco.languages.SignatureHelpContext
+ ) => {
+ const innerText = model.getValue();
+ const textRange = model.getFullModelRange();
+
+ const lengthAfterPosition = model.getValueLengthInRange({
+ startLineNumber: position.lineNumber,
+ startColumn: position.column,
+ endLineNumber: textRange.endLineNumber,
+ endColumn: textRange.endColumn,
+ });
+ return getSignatureHelp(
+ model.getValue(),
+ innerText.length - lengthAfterPosition,
+ operationDefinitionMap
+ );
+ },
+ [operationDefinitionMap]
+ );
+
+ const provideHover = useCallback(
+ async (
+ model: monaco.editor.ITextModel,
+ position: monaco.Position,
+ token: monaco.CancellationToken
+ ) => {
+ const innerText = model.getValue();
+ const textRange = model.getFullModelRange();
+
+ const lengthAfterPosition = model.getValueLengthInRange({
+ startLineNumber: position.lineNumber,
+ startColumn: position.column,
+ endLineNumber: textRange.endLineNumber,
+ endColumn: textRange.endColumn,
+ });
+ return getHover(
+ model.getValue(),
+ innerText.length - lengthAfterPosition,
+ operationDefinitionMap
+ );
+ },
+ [operationDefinitionMap]
+ );
+
+ const onTypeHandler = useCallback(
+ (e: monaco.editor.IModelContentChangedEvent, editor: monaco.editor.IStandaloneCodeEditor) => {
+ if (e.isFlush || e.isRedoing || e.isUndoing) {
+ return;
+ }
+ if (e.changes.length === 1 && e.changes[0].text === '=') {
+ const currentPosition = e.changes[0].range;
+ if (currentPosition) {
+ const tokenInfo = getTokenInfo(
+ editor.getValue(),
+ monacoPositionToOffset(
+ editor.getValue(),
+ new monaco.Position(currentPosition.startLineNumber, currentPosition.startColumn)
+ )
+ );
+ // Make sure that we are only adding kql='' or lucene='', and also
+ // check that the = sign isn't inside the KQL expression like kql='='
+ if (
+ !tokenInfo ||
+ typeof tokenInfo.ast === 'number' ||
+ tokenInfo.ast.type !== 'namedArgument' ||
+ (tokenInfo.ast.name !== 'kql' && tokenInfo.ast.name !== 'lucene') ||
+ tokenInfo.ast.value !== 'LENS_MATH_MARKER'
+ ) {
+ return;
+ }
+
+ // Timeout is required because otherwise the cursor position is not updated.
+ setTimeout(() => {
+ editor.executeEdits(
+ 'LENS',
+ [
+ {
+ range: {
+ ...currentPosition,
+ // Insert after the current char
+ startColumn: currentPosition.startColumn + 1,
+ endColumn: currentPosition.startColumn + 1,
+ },
+ text: `''`,
+ },
+ ],
+ [
+ // After inserting, move the cursor in between the single quotes
+ new monaco.Selection(
+ currentPosition.startLineNumber,
+ currentPosition.startColumn + 2,
+ currentPosition.startLineNumber,
+ currentPosition.startColumn + 2
+ ),
+ ]
+ );
+ editor.trigger('lens', 'editor.action.triggerSuggest', {});
+ }, 0);
+ }
+ }
+ },
+ []
+ );
+
+ const codeEditorOptions: CodeEditorProps = {
+ languageId: LANGUAGE_ID,
+ value: text ?? '',
+ onChange: setText,
+ options: {
+ automaticLayout: false,
+ fontSize: 14,
+ folding: false,
+ lineNumbers: 'off',
+ scrollBeyondLastLine: false,
+ minimap: { enabled: false },
+ wordWrap: 'on',
+ // Disable suggestions that appear when we don't provide a default suggestion
+ wordBasedSuggestions: false,
+ autoIndent: 'brackets',
+ wrappingIndent: 'none',
+ dimension: { width: 290, height: 200 },
+ fixedOverflowWidgets: true,
+ },
+ };
+
+ useEffect(() => {
+ // Because the monaco model is owned by Lens, we need to manually attach and remove handlers
+ const { dispose: dispose1 } = monaco.languages.registerCompletionItemProvider(LANGUAGE_ID, {
+ triggerCharacters: ['.', '(', '=', ' ', ':', `'`],
+ provideCompletionItems,
+ });
+ const { dispose: dispose2 } = monaco.languages.registerSignatureHelpProvider(LANGUAGE_ID, {
+ signatureHelpTriggerCharacters: ['(', '='],
+ provideSignatureHelp,
+ });
+ const { dispose: dispose3 } = monaco.languages.registerHoverProvider(LANGUAGE_ID, {
+ provideHover,
+ });
+ return () => {
+ dispose1();
+ dispose2();
+ dispose3();
+ };
+ }, [provideCompletionItems, provideSignatureHelp, provideHover]);
+
+ // The Monaco editor will lazily load Monaco, which takes a render cycle to trigger. This can cause differences
+ // in the behavior of Monaco when it's first loaded and then reloaded.
+ return (
+
+
+
+
+
+
+ {
+ toggleFullscreen();
+ }}
+ iconType="fullScreen"
+ size="s"
+ color="text"
+ flush="right"
+ >
+ {isFullscreen
+ ? i18n.translate('xpack.lens.formula.fullScreenCloseLabel', {
+ defaultMessage: 'Collapse formula',
+ })
+ : i18n.translate('xpack.lens.formula.fullScreenEditorLabel', {
+ defaultMessage: 'Expand formula',
+ })}
+
+
+
+
+
+ {
+ editor1.current = editor;
+ disposables.current.push(
+ editor.onDidFocusEditorWidget(() => {
+ setIsCloseable(false);
+ })
+ );
+ disposables.current.push(
+ editor.onDidBlurEditorWidget(() => {
+ setIsCloseable(true);
+ })
+ );
+ // If we ever introduce a second Monaco editor, we need to toggle
+ // the typing handler to the active editor to maintain the cursor
+ disposables.current.push(
+ editor.onDidChangeModelContent((e) => {
+ onTypeHandler(e, editor);
+ })
+ );
+ }}
+ />
+
+
+
+
+
+ {isFullscreen ? (
+
+ ) : (
+ setIsHelpOpen(false)}
+ button={
+ setIsHelpOpen(!isHelpOpen)}
+ iconType="help"
+ size="s"
+ color="text"
+ >
+ {i18n.translate('xpack.lens.formula.functionReferenceEditorLabel', {
+ defaultMessage: 'Function reference',
+ })}
+
+ }
+ anchorPosition="leftDown"
+ >
+
+
+ )}
+
+
+ {/* Errors go here */}
+
+
+
+ );
+}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx
new file mode 100644
index 0000000000000..1335cfe7e3efa
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx
@@ -0,0 +1,175 @@
+/*
+ * 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 React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiText,
+ EuiSelectable,
+ EuiSelectableOption,
+} from '@elastic/eui';
+import { Markdown } from '../../../../../../../../../src/plugins/kibana_react/public';
+import { GenericOperationDefinition, ParamEditorProps } from '../../index';
+import { IndexPattern } from '../../../../types';
+import { tinymathFunctions } from '../util';
+import { getPossibleFunctions } from './math_completion';
+
+import { FormulaIndexPatternColumn } from '../formula';
+
+function FormulaHelp({
+ indexPattern,
+ operationDefinitionMap,
+}: {
+ indexPattern: IndexPattern;
+ operationDefinitionMap: Record;
+}) {
+ const [selectedFunction, setSelectedFunction] = useState();
+
+ const helpItems: Array = [];
+
+ helpItems.push({ label: 'Math', isGroupLabel: true });
+
+ helpItems.push(
+ ...getPossibleFunctions(indexPattern)
+ .filter((key) => key in tinymathFunctions)
+ .map((key) => ({
+ label: `${key}`,
+ description: ,
+ checked: selectedFunction === key ? ('on' as const) : undefined,
+ }))
+ );
+
+ helpItems.push({ label: 'Elasticsearch', isGroupLabel: true });
+
+ // Es aggs
+ helpItems.push(
+ ...getPossibleFunctions(indexPattern)
+ .filter((key) => key in operationDefinitionMap)
+ .map((key) => ({
+ label: `${key}: ${operationDefinitionMap[key].displayName}`,
+ description: getHelpText(key, operationDefinitionMap),
+ checked:
+ selectedFunction === `${key}: ${operationDefinitionMap[key].displayName}`
+ ? ('on' as const)
+ : undefined,
+ }))
+ );
+
+ return (
+
+
+ {
+ const chosenType = newOptions.find(({ checked }) => checked === 'on')!;
+ if (!chosenType) {
+ setSelectedFunction(undefined);
+ } else {
+ setSelectedFunction(chosenType.label);
+ }
+ }}
+ >
+ {(list, search) => (
+ <>
+ {search}
+ {list}
+ >
+ )}
+
+
+
+
+ {selectedFunction ? (
+ helpItems.find(({ label }) => label === selectedFunction)?.description
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+export const MemoizedFormulaHelp = React.memo(FormulaHelp);
+
+// TODO: i18n this whole thing, or move examples into the operation definitions with i18n
+function getHelpText(
+ type: string,
+ operationDefinitionMap: ParamEditorProps['operationDefinitionMap']
+) {
+ const definition = operationDefinitionMap[type];
+
+ if (type === 'count') {
+ return (
+
+ Example: count()
+
+ );
+ }
+
+ return (
+
+ {definition.input === 'field' ? Example: {type}(bytes)
: null}
+ {definition.input === 'fullReference' && !('operationParams' in definition) ? (
+ Example: {type}(sum(bytes))
+ ) : null}
+
+ {'operationParams' in definition && definition.operationParams ? (
+
+
+ Example: {type}(sum(bytes),{' '}
+ {definition.operationParams.map((p) => `${p.name}=5`).join(', ')})
+
+
+ ) : null}
+
+ );
+}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/index.ts
new file mode 100644
index 0000000000000..4b6acefa6b30a
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/index.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export * from './formula_editor';
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math_completion.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
similarity index 100%
rename from x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math_completion.test.ts
rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math_completion.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts
similarity index 98%
rename from x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math_completion.ts
rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts
index 1ae5da9d6db1d..e8c16fe64651a 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math_completion.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts
@@ -15,10 +15,10 @@ import {
TinymathNamedArgument,
} from '@kbn/tinymath';
import { DataPublicPluginStart, QuerySuggestion } from 'src/plugins/data/public';
-import { IndexPattern } from '../../../types';
-import { memoizedGetAvailableOperationsByMetadata } from '../../operations';
-import { tinymathFunctions, groupArgsByType } from './util';
-import type { GenericOperationDefinition } from '..';
+import { IndexPattern } from '../../../../types';
+import { memoizedGetAvailableOperationsByMetadata } from '../../../operations';
+import { tinymathFunctions, groupArgsByType } from '../util';
+import type { GenericOperationDefinition } from '../..';
export enum SUGGESTION_TYPE {
FIELD = 'field',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math_tokenization.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_tokenization.tsx
similarity index 100%
rename from x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math_tokenization.tsx
rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_tokenization.tsx
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx
index 584ea5da38957..6f0abe8f55568 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx
@@ -5,61 +5,16 @@
* 2.0.
*/
-import React, { useCallback, useEffect, useState } from 'react';
-import { isObject } from 'lodash';
import { i18n } from '@kbn/i18n';
-import type { TinymathAST, TinymathVariable, TinymathLocation } from '@kbn/tinymath';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiText,
- EuiSpacer,
- EuiPopover,
- EuiSelectable,
- EuiSelectableOption,
-} from '@elastic/eui';
-import { monaco } from '@kbn/monaco';
-import classNames from 'classnames';
-import { CodeEditor, Markdown } from '../../../../../../../../src/plugins/kibana_react/public';
-import type { CodeEditorProps } from '../../../../../../../../src/plugins/kibana_react/public';
-import {
- OperationDefinition,
- GenericOperationDefinition,
- IndexPatternColumn,
- ParamEditorProps,
-} from '../index';
+import type { TinymathLocation } from '@kbn/tinymath';
+import { OperationDefinition, GenericOperationDefinition } from '../index';
import { ReferenceBasedIndexPatternColumn } from '../column_types';
import { IndexPattern, IndexPatternLayer } from '../../../types';
-import { getColumnOrder, getManagedColumnsFrom } from '../../layer_helpers';
-import { mathOperation } from './math';
-import { documentField } from '../../../document_field';
-import { ErrorWrapper, runASTValidation, shouldHaveFieldArgument, tryToParse } from './validation';
-import {
- extractParamsForFormula,
- findVariables,
- getOperationParams,
- getSafeFieldName,
- groupArgsByType,
- hasMathNode,
- tinymathFunctions,
-} from './util';
-import { useDebounceWithOptions } from '../helpers';
-import {
- LensMathSuggestion,
- SUGGESTION_TYPE,
- suggest,
- getSuggestion,
- getPossibleFunctions,
- getSignatureHelp,
- getHover,
- getTokenInfo,
- offsetToRowColumn,
- monacoPositionToOffset,
-} from './math_completion';
-import { LANGUAGE_ID } from './math_tokenization';
-
-import './formula.scss';
+import { getColumnOrder } from '../../layer_helpers';
+import { runASTValidation, tryToParse } from './validation';
+import { FormulaEditor } from './editor';
+import { parseAndExtract } from './parse';
+import { generateFormula } from './generate';
const defaultLabel = i18n.translate('xpack.lens.indexPattern.formulaLabel', {
defaultMessage: 'Formula',
@@ -153,41 +108,12 @@ export const formulaOperation: OperationDefinition<
buildColumn({ previousColumn, layer, indexPattern }, _, operationDefinitionMap) {
let previousFormula = '';
if (previousColumn) {
- if ('references' in previousColumn) {
- const metric = layer.columns[previousColumn.references[0]];
- if (metric && 'sourceField' in metric && metric.dataType === 'number') {
- const fieldName = getSafeFieldName(metric.sourceField);
- // TODO need to check the input type from the definition
- previousFormula += `${previousColumn.operationType}(${metric.operationType}(${fieldName})`;
- }
- } else {
- if (
- previousColumn &&
- 'sourceField' in previousColumn &&
- previousColumn.dataType === 'number'
- ) {
- previousFormula += `${previousColumn.operationType}(${getSafeFieldName(
- previousColumn?.sourceField
- )}`;
- }
- }
- const formulaNamedArgs = extractParamsForFormula(previousColumn, operationDefinitionMap);
- if (formulaNamedArgs.length) {
- previousFormula +=
- ', ' + formulaNamedArgs.map(({ name, value }) => `${name}=${value}`).join(', ');
- }
- if (previousColumn.filter) {
- if (previousColumn.operationType !== 'count') {
- previousFormula += ', ';
- }
- previousFormula +=
- (previousColumn.filter.language === 'kuery' ? 'kql=' : 'lucene=') +
- `'${previousColumn.filter.query.replace(/'/g, `\\'`)}'`; // replace all
- }
- if (previousFormula) {
- // close the formula at the end
- previousFormula += ')';
- }
+ previousFormula = generateFormula(
+ previousColumn,
+ layer,
+ previousFormula,
+ operationDefinitionMap
+ );
}
// carry over the format settings from previous operation for seamless transfer
// NOTE: this works only for non-default formatters set in Lens
@@ -207,11 +133,8 @@ export const formulaOperation: OperationDefinition<
references: [],
};
},
- isTransferable: (column, newIndexPattern, operationDefinitionMap) => {
- // Basic idea: if it has any math operation in it, probably it cannot be transferable
- const { root, error } = tryToParse(column.params.formula || '');
- if (!root) return true;
- return Boolean(!error && !hasMathNode(root));
+ isTransferable: () => {
+ return true;
},
createCopy(layer, sourceId, targetId, indexPattern, operationDefinitionMap) {
const currentColumn = layer.columns[sourceId] as FormulaIndexPatternColumn;
@@ -236,660 +159,6 @@ export const formulaOperation: OperationDefinition<
paramEditor: FormulaEditor,
};
-function FormulaEditor({
- layer,
- updateLayer,
- currentColumn,
- columnId,
- indexPattern,
- operationDefinitionMap,
- data,
- toggleFullscreen,
- isFullscreen,
- setIsCloseable,
-}: ParamEditorProps) {
- const [text, setText] = useState(currentColumn.params.formula);
- const [isHelpOpen, setIsHelpOpen] = useState(false);
- const editorModel = React.useRef(
- monaco.editor.createModel(text ?? '', LANGUAGE_ID)
- );
- const overflowDiv1 = React.useRef();
- const disposables = React.useRef([]);
- const editor1 = React.useRef();
-
- // The Monaco editor needs to have the overflowDiv in the first render. Using an effect
- // requires a second render to work, so we are using an if statement to guarantee it happens
- // on first render
- if (!overflowDiv1?.current) {
- const node1 = (overflowDiv1.current = document.createElement('div'));
- node1.setAttribute('data-test-subj', 'lnsFormulaWidget');
- // Monaco CSS is targeted on the monaco-editor class
- node1.classList.add('lnsFormulaOverflow', 'monaco-editor');
- document.body.appendChild(node1);
- }
-
- // Clean up the monaco editor and DOM on unmount
- useEffect(() => {
- const model = editorModel.current;
- const allDisposables = disposables.current;
- const editor1ref = editor1.current;
- return () => {
- model.dispose();
- overflowDiv1.current?.parentNode?.removeChild(overflowDiv1.current);
- editor1ref?.dispose();
- allDisposables?.forEach((d) => d.dispose());
- };
- }, []);
-
- useDebounceWithOptions(
- () => {
- if (!editorModel.current) return;
-
- if (!text) {
- monaco.editor.setModelMarkers(editorModel.current, 'LENS', []);
- if (currentColumn.params.formula) {
- // Only submit if valid
- const { newLayer } = regenerateLayerFromAst(
- text || '',
- layer,
- columnId,
- currentColumn,
- indexPattern,
- operationDefinitionMap
- );
- updateLayer(newLayer);
- }
-
- return;
- }
-
- let errors: ErrorWrapper[] = [];
-
- const { root, error } = tryToParse(text);
- if (error) {
- errors = [error];
- } else if (root) {
- const validationErrors = runASTValidation(
- root,
- layer,
- indexPattern,
- operationDefinitionMap
- );
- if (validationErrors.length) {
- errors = validationErrors;
- }
- }
-
- if (errors.length) {
- monaco.editor.setModelMarkers(
- editorModel.current,
- 'LENS',
- errors.flatMap((innerError) => {
- if (innerError.locations.length) {
- return innerError.locations.map((location) => {
- const startPosition = offsetToRowColumn(text, location.min);
- const endPosition = offsetToRowColumn(text, location.max);
- return {
- message: innerError.message,
- startColumn: startPosition.column + 1,
- startLineNumber: startPosition.lineNumber,
- endColumn: endPosition.column + 1,
- endLineNumber: endPosition.lineNumber,
- severity:
- innerError.severity === 'warning'
- ? monaco.MarkerSeverity.Warning
- : monaco.MarkerSeverity.Error,
- };
- });
- } else {
- // Parse errors return no location info
- const startPosition = offsetToRowColumn(text, 0);
- const endPosition = offsetToRowColumn(text, text.length - 1);
- return [
- {
- message: innerError.message,
- startColumn: startPosition.column + 1,
- startLineNumber: startPosition.lineNumber,
- endColumn: endPosition.column + 1,
- endLineNumber: endPosition.lineNumber,
- severity:
- innerError.severity === 'warning'
- ? monaco.MarkerSeverity.Warning
- : monaco.MarkerSeverity.Error,
- },
- ];
- }
- })
- );
- } else {
- monaco.editor.setModelMarkers(editorModel.current, 'LENS', []);
-
- // Only submit if valid
- const { newLayer, locations } = regenerateLayerFromAst(
- text || '',
- layer,
- columnId,
- currentColumn,
- indexPattern,
- operationDefinitionMap
- );
- updateLayer(newLayer);
-
- const managedColumns = getManagedColumnsFrom(columnId, newLayer.columns);
- const markers: monaco.editor.IMarkerData[] = managedColumns
- .flatMap(([id, column]) => {
- if (locations[id]) {
- const def = operationDefinitionMap[column.operationType];
- if (def.getErrorMessage) {
- const messages = def.getErrorMessage(
- newLayer,
- id,
- indexPattern,
- operationDefinitionMap
- );
- if (messages) {
- const startPosition = offsetToRowColumn(text, locations[id].min);
- const endPosition = offsetToRowColumn(text, locations[id].max);
- return [
- {
- message: messages.join(', '),
- startColumn: startPosition.column + 1,
- startLineNumber: startPosition.lineNumber,
- endColumn: endPosition.column + 1,
- endLineNumber: endPosition.lineNumber,
- severity: monaco.MarkerSeverity.Warning,
- },
- ];
- }
- }
- }
- return [];
- })
- .filter((marker) => marker);
- monaco.editor.setModelMarkers(editorModel.current, 'LENS', markers);
- }
- },
- // Make it validate on flyout open in case of a broken formula left over
- // from a previous edit
- { skipFirstRender: text == null },
- 256,
- [text]
- );
-
- /**
- * The way that Monaco requests autocompletion is not intuitive, but the way we use it
- * we fetch new suggestions in these scenarios:
- *
- * - If the user types one of the trigger characters, suggestions are always fetched
- * - When the user selects the kql= suggestion, we tell Monaco to trigger new suggestions after
- * - When the user types the first character into an empty text box, Monaco requests suggestions
- *
- * Monaco also triggers suggestions automatically when there are no suggestions being displayed
- * and the user types a non-whitespace character.
- *
- * While suggestions are being displayed, Monaco uses an in-memory cache of the last known suggestions.
- */
- const provideCompletionItems = useCallback(
- async (
- model: monaco.editor.ITextModel,
- position: monaco.Position,
- context: monaco.languages.CompletionContext
- ) => {
- const innerText = model.getValue();
- const textRange = model.getFullModelRange();
- let wordRange: monaco.Range;
- let aSuggestions: { list: LensMathSuggestion[]; type: SUGGESTION_TYPE } = {
- list: [],
- type: SUGGESTION_TYPE.FIELD,
- };
-
- const lengthAfterPosition = model.getValueLengthInRange({
- startLineNumber: position.lineNumber,
- startColumn: position.column,
- endLineNumber: textRange.endLineNumber,
- endColumn: textRange.endColumn,
- });
-
- 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 = await suggest({
- expression: innerText.substring(0, innerText.length - lengthAfterPosition) + ')',
- position: innerText.length - lengthAfterPosition,
- context,
- indexPattern,
- operationDefinitionMap,
- data,
- });
- }
- } else {
- aSuggestions = await suggest({
- expression: innerText,
- position: innerText.length - lengthAfterPosition,
- context,
- indexPattern,
- operationDefinitionMap,
- data,
- });
- }
-
- return {
- suggestions: aSuggestions.list.map((s) =>
- getSuggestion(s, aSuggestions.type, wordRange, operationDefinitionMap)
- ),
- };
- },
- [indexPattern, operationDefinitionMap, data]
- );
-
- const provideSignatureHelp = useCallback(
- async (
- model: monaco.editor.ITextModel,
- position: monaco.Position,
- token: monaco.CancellationToken,
- context: monaco.languages.SignatureHelpContext
- ) => {
- const innerText = model.getValue();
- const textRange = model.getFullModelRange();
-
- const lengthAfterPosition = model.getValueLengthInRange({
- startLineNumber: position.lineNumber,
- startColumn: position.column,
- endLineNumber: textRange.endLineNumber,
- endColumn: textRange.endColumn,
- });
- return getSignatureHelp(
- model.getValue(),
- innerText.length - lengthAfterPosition,
- operationDefinitionMap
- );
- },
- [operationDefinitionMap]
- );
-
- const provideHover = useCallback(
- async (
- model: monaco.editor.ITextModel,
- position: monaco.Position,
- token: monaco.CancellationToken
- ) => {
- const innerText = model.getValue();
- const textRange = model.getFullModelRange();
-
- const lengthAfterPosition = model.getValueLengthInRange({
- startLineNumber: position.lineNumber,
- startColumn: position.column,
- endLineNumber: textRange.endLineNumber,
- endColumn: textRange.endColumn,
- });
- return getHover(
- model.getValue(),
- innerText.length - lengthAfterPosition,
- operationDefinitionMap
- );
- },
- [operationDefinitionMap]
- );
-
- const onTypeHandler = useCallback(
- (e: monaco.editor.IModelContentChangedEvent, editor: monaco.editor.IStandaloneCodeEditor) => {
- if (e.isFlush || e.isRedoing || e.isUndoing) {
- return;
- }
- if (e.changes.length === 1 && e.changes[0].text === '=') {
- const currentPosition = e.changes[0].range;
- if (currentPosition) {
- const tokenInfo = getTokenInfo(
- editor.getValue(),
- monacoPositionToOffset(
- editor.getValue(),
- new monaco.Position(currentPosition.startLineNumber, currentPosition.startColumn)
- )
- );
- // Make sure that we are only adding kql='' or lucene='', and also
- // check that the = sign isn't inside the KQL expression like kql='='
- if (
- !tokenInfo ||
- typeof tokenInfo.ast === 'number' ||
- tokenInfo.ast.type !== 'namedArgument' ||
- (tokenInfo.ast.name !== 'kql' && tokenInfo.ast.name !== 'lucene') ||
- tokenInfo.ast.value !== 'LENS_MATH_MARKER'
- ) {
- return;
- }
-
- // Timeout is required because otherwise the cursor position is not updated.
- setTimeout(() => {
- editor.executeEdits(
- 'LENS',
- [
- {
- range: {
- ...currentPosition,
- // Insert after the current char
- startColumn: currentPosition.startColumn + 1,
- endColumn: currentPosition.startColumn + 1,
- },
- text: `''`,
- },
- ],
- [
- // After inserting, move the cursor in between the single quotes
- new monaco.Selection(
- currentPosition.startLineNumber,
- currentPosition.startColumn + 2,
- currentPosition.startLineNumber,
- currentPosition.startColumn + 2
- ),
- ]
- );
- editor.trigger('lens', 'editor.action.triggerSuggest', {});
- }, 0);
- }
- }
- },
- []
- );
-
- const codeEditorOptions: CodeEditorProps = {
- languageId: LANGUAGE_ID,
- value: text ?? '',
- onChange: setText,
- options: {
- automaticLayout: false,
- fontSize: 14,
- folding: false,
- lineNumbers: 'off',
- scrollBeyondLastLine: false,
- minimap: { enabled: false },
- wordWrap: 'on',
- // Disable suggestions that appear when we don't provide a default suggestion
- wordBasedSuggestions: false,
- autoIndent: 'brackets',
- wrappingIndent: 'none',
- dimension: { width: 290, height: 200 },
- fixedOverflowWidgets: true,
- },
- };
-
- useEffect(() => {
- // Because the monaco model is owned by Lens, we need to manually attach and remove handlers
- const { dispose: dispose1 } = monaco.languages.registerCompletionItemProvider(LANGUAGE_ID, {
- triggerCharacters: ['.', '(', '=', ' ', ':', `'`],
- provideCompletionItems,
- });
- const { dispose: dispose2 } = monaco.languages.registerSignatureHelpProvider(LANGUAGE_ID, {
- signatureHelpTriggerCharacters: ['(', '='],
- provideSignatureHelp,
- });
- const { dispose: dispose3 } = monaco.languages.registerHoverProvider(LANGUAGE_ID, {
- provideHover,
- });
- return () => {
- dispose1();
- dispose2();
- dispose3();
- };
- }, [provideCompletionItems, provideSignatureHelp, provideHover]);
-
- // The Monaco editor will lazily load Monaco, which takes a render cycle to trigger. This can cause differences
- // in the behavior of Monaco when it's first loaded and then reloaded.
- return (
-
-
-
-
-
-
- {
- toggleFullscreen();
- }}
- iconType="fullScreen"
- size="s"
- color="text"
- flush="right"
- >
- {isFullscreen
- ? i18n.translate('xpack.lens.formula.fullScreenCloseLabel', {
- defaultMessage: 'Collapse formula',
- })
- : i18n.translate('xpack.lens.formula.fullScreenEditorLabel', {
- defaultMessage: 'Expand formula',
- })}
-
-
-
-
-
- {
- editor1.current = editor;
- disposables.current.push(
- editor.onDidFocusEditorWidget(() => {
- setIsCloseable(false);
- })
- );
- disposables.current.push(
- editor.onDidBlurEditorWidget(() => {
- setIsCloseable(true);
- })
- );
- // If we ever introduce a second Monaco editor, we need to toggle
- // the typing handler to the active editor to maintain the cursor
- disposables.current.push(
- editor.onDidChangeModelContent((e) => {
- onTypeHandler(e, editor);
- })
- );
- }}
- />
-
-
-
-
-
- {isFullscreen ? (
-
- ) : (
- setIsHelpOpen(false)}
- button={
- setIsHelpOpen(!isHelpOpen)}
- iconType="help"
- size="s"
- color="text"
- >
- {i18n.translate('xpack.lens.formula.functionReferenceEditorLabel', {
- defaultMessage: 'Function reference',
- })}
-
- }
- anchorPosition="leftDown"
- >
-
-
- )}
-
-
- {/* Errors go here */}
-
-
-
- );
-}
-
-function FormulaHelp({
- indexPattern,
- operationDefinitionMap,
-}: {
- indexPattern: IndexPattern;
- operationDefinitionMap: Record;
-}) {
- const [selectedFunction, setSelectedFunction] = useState();
-
- const helpItems: Array = [];
-
- helpItems.push({ label: 'Math', isGroupLabel: true });
-
- helpItems.push(
- ...getPossibleFunctions(indexPattern)
- .filter((key) => key in tinymathFunctions)
- .map((key) => ({
- label: `${key}`,
- description: ,
- checked: selectedFunction === key ? ('on' as const) : undefined,
- }))
- );
-
- helpItems.push({ label: 'Elasticsearch', isGroupLabel: true });
-
- // Es aggs
- helpItems.push(
- ...getPossibleFunctions(indexPattern)
- .filter((key) => key in operationDefinitionMap)
- .map((key) => ({
- label: `${key}: ${operationDefinitionMap[key].displayName}`,
- description: getHelpText(key, operationDefinitionMap),
- checked:
- selectedFunction === `${key}: ${operationDefinitionMap[key].displayName}`
- ? ('on' as const)
- : undefined,
- }))
- );
-
- return (
-
-
- {
- const chosenType = newOptions.find(({ checked }) => checked === 'on')!;
- if (!chosenType) {
- setSelectedFunction(undefined);
- } else {
- setSelectedFunction(chosenType.label);
- }
- }}
- >
- {(list, search) => (
- <>
- {search}
- {list}
- >
- )}
-
-
-
-
- {selectedFunction ? (
- helpItems.find(({ label }) => label === selectedFunction)?.description
- ) : (
-
- )}
-
-
-
- );
-}
-
-const MemoizedFormulaHelp = React.memo(FormulaHelp);
-
-function parseAndExtract(
- text: string,
- layer: IndexPatternLayer,
- columnId: string,
- indexPattern: IndexPattern,
- operationDefinitionMap: Record
-) {
- const { root, error } = tryToParse(text);
- if (error || !root) {
- return { extracted: [], isValid: false };
- }
- // before extracting the data run the validation task and throw if invalid
- const errors = runASTValidation(root, layer, indexPattern, operationDefinitionMap);
- if (errors.length) {
- return { extracted: [], isValid: false };
- }
- /*
- { name: 'add', args: [ { name: 'abc', args: [5] }, 5 ] }
- */
- const extracted = extractColumns(columnId, operationDefinitionMap, root, layer, indexPattern);
- return { extracted, isValid: true };
-}
-
export function regenerateLayerFromAst(
text: string,
layer: IndexPatternLayer,
@@ -947,149 +216,3 @@ export function regenerateLayerFromAst(
// turn ast into referenced columns
// set state
}
-
-function extractColumns(
- idPrefix: string,
- operations: Record,
- ast: TinymathAST,
- layer: IndexPatternLayer,
- indexPattern: IndexPattern
-): Array<{ column: IndexPatternColumn; location?: TinymathLocation }> {
- const columns: Array<{ column: IndexPatternColumn; location?: TinymathLocation }> = [];
-
- function parseNode(node: TinymathAST) {
- if (typeof node === 'number' || node.type !== 'function') {
- // leaf node
- return node;
- }
-
- const nodeOperation = operations[node.name];
- if (!nodeOperation) {
- // it's a regular math node
- const consumedArgs = node.args.map(parseNode).filter(Boolean) as Array<
- number | TinymathVariable
- >;
- return {
- ...node,
- args: consumedArgs,
- };
- }
-
- // split the args into types for better TS experience
- const { namedArguments, variables, functions } = groupArgsByType(node.args);
-
- // operation node
- if (nodeOperation.input === 'field') {
- const [fieldName] = variables.filter((v): v is TinymathVariable => isObject(v));
- // a validation task passed before executing this and checked already there's a field
- const field = shouldHaveFieldArgument(node)
- ? indexPattern.getFieldByName(fieldName.value)!
- : documentField;
-
- const mappedParams = getOperationParams(nodeOperation, namedArguments || []);
-
- const newCol = (nodeOperation as OperationDefinition<
- IndexPatternColumn,
- 'field'
- >).buildColumn(
- {
- layer,
- indexPattern,
- field,
- },
- mappedParams
- );
- const newColId = `${idPrefix}X${columns.length}`;
- newCol.customLabel = true;
- newCol.label = newColId;
- columns.push({ column: newCol, location: node.location });
- // replace by new column id
- return newColId;
- }
-
- if (nodeOperation.input === 'fullReference') {
- const [referencedOp] = functions;
- const consumedParam = parseNode(referencedOp);
-
- const subNodeVariables = consumedParam ? findVariables(consumedParam) : [];
- const mathColumn = mathOperation.buildColumn({
- layer,
- indexPattern,
- });
- mathColumn.references = subNodeVariables.map(({ value }) => value);
- mathColumn.params.tinymathAst = consumedParam!;
- columns.push({ column: mathColumn });
- mathColumn.customLabel = true;
- mathColumn.label = `${idPrefix}X${columns.length - 1}`;
-
- const mappedParams = getOperationParams(nodeOperation, namedArguments || []);
- const newCol = (nodeOperation as OperationDefinition<
- IndexPatternColumn,
- 'fullReference'
- >).buildColumn(
- {
- layer,
- indexPattern,
- referenceIds: [`${idPrefix}X${columns.length - 1}`],
- },
- mappedParams
- );
- const newColId = `${idPrefix}X${columns.length}`;
- newCol.customLabel = true;
- newCol.label = newColId;
- columns.push({ column: newCol, location: node.location });
- // replace by new column id
- return newColId;
- }
- }
- const root = parseNode(ast);
- if (root === undefined) {
- return [];
- }
- const variables = findVariables(root);
- const mathColumn = mathOperation.buildColumn({
- layer,
- indexPattern,
- });
- mathColumn.references = variables.map(({ value }) => value);
- mathColumn.params.tinymathAst = root!;
- const newColId = `${idPrefix}X${columns.length}`;
- mathColumn.customLabel = true;
- mathColumn.label = newColId;
- columns.push({ column: mathColumn });
- return columns;
-}
-
-// TODO: i18n this whole thing, or move examples into the operation definitions with i18n
-function getHelpText(
- type: string,
- operationDefinitionMap: ParamEditorProps['operationDefinitionMap']
-) {
- const definition = operationDefinitionMap[type];
-
- if (type === 'count') {
- return (
-
- Example: count()
-
- );
- }
-
- return (
-
- {definition.input === 'field' ? Example: {type}(bytes)
: null}
- {definition.input === 'fullReference' && !('operationParams' in definition) ? (
- Example: {type}(sum(bytes))
- ) : null}
-
- {'operationParams' in definition && definition.operationParams ? (
-
-
- Example: {type}(sum(bytes),{' '}
- {definition.operationParams.map((p) => `${p.name}=5`).join(', ')})
-
-
- ) : null}
-
- );
-}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts
new file mode 100644
index 0000000000000..e44cd50ae9c41
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts
@@ -0,0 +1,90 @@
+/*
+ * 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 { isObject } from 'lodash';
+import { GenericOperationDefinition, IndexPatternColumn } from '../index';
+import { ReferenceBasedIndexPatternColumn } from '../column_types';
+import { IndexPatternLayer } from '../../../types';
+
+// Just handle two levels for now
+type OperationParams = Record>;
+
+export function getSafeFieldName(fieldName: string | undefined) {
+ // clean up the "Records" field for now
+ if (!fieldName || fieldName === 'Records') {
+ return '';
+ }
+ return fieldName;
+}
+
+export function generateFormula(
+ previousColumn: ReferenceBasedIndexPatternColumn | IndexPatternColumn,
+ layer: IndexPatternLayer,
+ previousFormula: string,
+ operationDefinitionMap: Record | undefined
+) {
+ if ('references' in previousColumn) {
+ const metric = layer.columns[previousColumn.references[0]];
+ if (metric && 'sourceField' in metric && metric.dataType === 'number') {
+ const fieldName = getSafeFieldName(metric.sourceField);
+ // TODO need to check the input type from the definition
+ previousFormula += `${previousColumn.operationType}(${metric.operationType}(${fieldName})`;
+ }
+ } else {
+ if (previousColumn && 'sourceField' in previousColumn && previousColumn.dataType === 'number') {
+ previousFormula += `${previousColumn.operationType}(${getSafeFieldName(
+ previousColumn?.sourceField
+ )}`;
+ }
+ }
+ const formulaNamedArgs = extractParamsForFormula(previousColumn, operationDefinitionMap);
+ if (formulaNamedArgs.length) {
+ previousFormula +=
+ ', ' + formulaNamedArgs.map(({ name, value }) => `${name}=${value}`).join(', ');
+ }
+ if (previousColumn.filter) {
+ if (previousColumn.operationType !== 'count') {
+ previousFormula += ', ';
+ }
+ previousFormula +=
+ (previousColumn.filter.language === 'kuery' ? 'kql=' : 'lucene=') +
+ `'${previousColumn.filter.query.replace(/'/g, `\\'`)}'`; // replace all
+ }
+ if (previousFormula) {
+ // close the formula at the end
+ previousFormula += ')';
+ }
+ return previousFormula;
+}
+
+function extractParamsForFormula(
+ column: IndexPatternColumn | ReferenceBasedIndexPatternColumn,
+ operationDefinitionMap: Record | undefined
+) {
+ if (!operationDefinitionMap) {
+ return [];
+ }
+ const def = operationDefinitionMap[column.operationType];
+ if ('operationParams' in def && column.params) {
+ return (def.operationParams || []).flatMap(({ name, required }) => {
+ const value = (column.params as OperationParams)![name];
+ if (isObject(value)) {
+ return Object.keys(value).map((subName) => ({
+ name: `${name}-${subName}`,
+ value: value[subName] as string | number,
+ required,
+ }));
+ }
+ return {
+ name,
+ value,
+ required,
+ };
+ });
+ }
+ return [];
+}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts
new file mode 100644
index 0000000000000..9ddc1973044f8
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts
@@ -0,0 +1,150 @@
+/*
+ * 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 { isObject } from 'lodash';
+import type { TinymathAST, TinymathVariable, TinymathLocation } from '@kbn/tinymath';
+import { OperationDefinition, GenericOperationDefinition, IndexPatternColumn } from '../index';
+import { IndexPattern, IndexPatternLayer } from '../../../types';
+import { mathOperation } from './math';
+import { documentField } from '../../../document_field';
+import { runASTValidation, shouldHaveFieldArgument, tryToParse } from './validation';
+import { findVariables, getOperationParams, groupArgsByType } from './util';
+
+export function parseAndExtract(
+ text: string,
+ layer: IndexPatternLayer,
+ columnId: string,
+ indexPattern: IndexPattern,
+ operationDefinitionMap: Record
+) {
+ const { root, error } = tryToParse(text);
+ if (error || !root) {
+ return { extracted: [], isValid: false };
+ }
+ // before extracting the data run the validation task and throw if invalid
+ const errors = runASTValidation(root, layer, indexPattern, operationDefinitionMap);
+ if (errors.length) {
+ return { extracted: [], isValid: false };
+ }
+ /*
+ { name: 'add', args: [ { name: 'abc', args: [5] }, 5 ] }
+ */
+ const extracted = extractColumns(columnId, operationDefinitionMap, root, layer, indexPattern);
+ return { extracted, isValid: true };
+}
+
+function extractColumns(
+ idPrefix: string,
+ operations: Record,
+ ast: TinymathAST,
+ layer: IndexPatternLayer,
+ indexPattern: IndexPattern
+): Array<{ column: IndexPatternColumn; location?: TinymathLocation }> {
+ const columns: Array<{ column: IndexPatternColumn; location?: TinymathLocation }> = [];
+
+ function parseNode(node: TinymathAST) {
+ if (typeof node === 'number' || node.type !== 'function') {
+ // leaf node
+ return node;
+ }
+
+ const nodeOperation = operations[node.name];
+ if (!nodeOperation) {
+ // it's a regular math node
+ const consumedArgs = node.args.map(parseNode).filter(Boolean) as Array<
+ number | TinymathVariable
+ >;
+ return {
+ ...node,
+ args: consumedArgs,
+ };
+ }
+
+ // split the args into types for better TS experience
+ const { namedArguments, variables, functions } = groupArgsByType(node.args);
+
+ // operation node
+ if (nodeOperation.input === 'field') {
+ const [fieldName] = variables.filter((v): v is TinymathVariable => isObject(v));
+ // a validation task passed before executing this and checked already there's a field
+ const field = shouldHaveFieldArgument(node)
+ ? indexPattern.getFieldByName(fieldName.value)!
+ : documentField;
+
+ const mappedParams = getOperationParams(nodeOperation, namedArguments || []);
+
+ const newCol = (nodeOperation as OperationDefinition<
+ IndexPatternColumn,
+ 'field'
+ >).buildColumn(
+ {
+ layer,
+ indexPattern,
+ field,
+ },
+ mappedParams
+ );
+ const newColId = `${idPrefix}X${columns.length}`;
+ newCol.customLabel = true;
+ newCol.label = newColId;
+ columns.push({ column: newCol, location: node.location });
+ // replace by new column id
+ return newColId;
+ }
+
+ if (nodeOperation.input === 'fullReference') {
+ const [referencedOp] = functions;
+ const consumedParam = parseNode(referencedOp);
+
+ const subNodeVariables = consumedParam ? findVariables(consumedParam) : [];
+ const mathColumn = mathOperation.buildColumn({
+ layer,
+ indexPattern,
+ });
+ mathColumn.references = subNodeVariables.map(({ value }) => value);
+ mathColumn.params.tinymathAst = consumedParam!;
+ columns.push({ column: mathColumn });
+ mathColumn.customLabel = true;
+ mathColumn.label = `${idPrefix}X${columns.length - 1}`;
+
+ const mappedParams = getOperationParams(nodeOperation, namedArguments || []);
+ const newCol = (nodeOperation as OperationDefinition<
+ IndexPatternColumn,
+ 'fullReference'
+ >).buildColumn(
+ {
+ layer,
+ indexPattern,
+ referenceIds: [`${idPrefix}X${columns.length - 1}`],
+ },
+ mappedParams
+ );
+ const newColId = `${idPrefix}X${columns.length}`;
+ newCol.customLabel = true;
+ newCol.label = newColId;
+ columns.push({ column: newCol, location: node.location });
+ // replace by new column id
+ return newColId;
+ }
+ }
+ const root = parseNode(ast);
+ if (root === undefined) {
+ return [];
+ }
+ const variables = findVariables(root);
+ const mathColumn = mathOperation.buildColumn({
+ layer,
+ indexPattern,
+ });
+ mathColumn.references = variables.map(({ value }) => value);
+ mathColumn.params.tinymathAst = root!;
+ const newColId = `${idPrefix}X${columns.length}`;
+ mathColumn.customLabel = true;
+ mathColumn.label = newColId;
+ columns.push({ column: mathColumn });
+ return columns;
+}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts
index 17ca19839a216..5d9a8647eb7ab 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts
@@ -10,12 +10,10 @@ import { i18n } from '@kbn/i18n';
import type {
TinymathAST,
TinymathFunction,
- TinymathLocation,
TinymathNamedArgument,
TinymathVariable,
} from 'packages/kbn-tinymath';
-import { ReferenceBasedIndexPatternColumn } from '../column_types';
-import type { OperationDefinition, IndexPatternColumn, GenericOperationDefinition } from '../index';
+import type { OperationDefinition, IndexPatternColumn } from '../index';
import type { GroupedNodes } from './types';
export function groupArgsByType(args: TinymathAST[]) {
@@ -43,45 +41,6 @@ export function getValueOrName(node: TinymathAST) {
return node.name;
}
-export function getSafeFieldName(fieldName: string | undefined) {
- // clean up the "Records" field for now
- if (!fieldName || fieldName === 'Records') {
- return '';
- }
- return fieldName;
-}
-
-// Just handle two levels for now
-type OeprationParams = Record>;
-
-export function extractParamsForFormula(
- column: IndexPatternColumn | ReferenceBasedIndexPatternColumn,
- operationDefinitionMap: Record | undefined
-) {
- if (!operationDefinitionMap) {
- return [];
- }
- const def = operationDefinitionMap[column.operationType];
- if ('operationParams' in def && column.params) {
- return (def.operationParams || []).flatMap(({ name, required }) => {
- const value = (column.params as OeprationParams)![name];
- if (isObject(value)) {
- return Object.keys(value).map((subName) => ({
- name: `${name}-${subName}`,
- value: value[subName] as string | number,
- required,
- }));
- }
- return {
- name,
- value,
- required,
- };
- });
- }
- return [];
-}
-
export function getOperationParams(
operation:
| OperationDefinition
@@ -332,32 +291,6 @@ export function findMathNodes(root: TinymathAST | string): TinymathFunction[] {
return flattenMathNodes(root);
}
-export function hasMathNode(root: TinymathAST): boolean {
- return Boolean(findMathNodes(root).length);
-}
-
-function findFunctionNodes(root: TinymathAST | string): TinymathFunction[] {
- function flattenFunctionNodes(node: TinymathAST | string): TinymathFunction[] {
- if (!isObject(node) || node.type !== 'function') {
- return [];
- }
- return [node, ...node.args.flatMap(flattenFunctionNodes)].filter(Boolean);
- }
- return flattenFunctionNodes(root);
-}
-
-export function hasInvalidOperations(
- node: TinymathAST | string,
- operations: Record
-): { names: string[]; locations: TinymathLocation[] } {
- const nodes = findFunctionNodes(node).filter((v) => !isMathNode(v) && !operations[v.name]);
- return {
- // avoid duplicates
- names: Array.from(new Set(nodes.map(({ name }) => name))),
- locations: nodes.map(({ location }) => location),
- };
-}
-
// traverse a tree and find all string leaves
export function findVariables(node: TinymathAST | string): TinymathVariable[] {
if (typeof node === 'string') {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts
index cb52e22302cbe..4e5ae21e576e4 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts
@@ -16,7 +16,6 @@ import {
getOperationParams,
getValueOrName,
groupArgsByType,
- hasInvalidOperations,
isMathNode,
tinymathFunctions,
} from './util';
@@ -74,6 +73,28 @@ export function isParsingError(message: string) {
return message.includes(validationErrors.failedParsing.message);
}
+function findFunctionNodes(root: TinymathAST | string): TinymathFunction[] {
+ function flattenFunctionNodes(node: TinymathAST | string): TinymathFunction[] {
+ if (!isObject(node) || node.type !== 'function') {
+ return [];
+ }
+ return [node, ...node.args.flatMap(flattenFunctionNodes)].filter(Boolean);
+ }
+ return flattenFunctionNodes(root);
+}
+
+export function hasInvalidOperations(
+ node: TinymathAST | string,
+ operations: Record
+): { names: string[]; locations: TinymathLocation[] } {
+ const nodes = findFunctionNodes(node).filter((v) => !isMathNode(v) && !operations[v.name]);
+ return {
+ // avoid duplicates
+ names: Array.from(new Set(nodes.map(({ name }) => name))),
+ locations: nodes.map(({ location }) => location),
+ };
+}
+
export const getQueryValidationError = (
query: string,
language: 'kql' | 'lucene',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
index 49366f2421b7b..4fd429820379f 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
@@ -1079,11 +1079,21 @@ export function getExistingColumnGroups(layer: IndexPatternLayer): [string[], st
/**
* Returns true if the given column can be applied to the given index pattern
*/
-export function isColumnTransferable(column: IndexPatternColumn, newIndexPattern: IndexPattern) {
- return operationDefinitionMap[column.operationType].isTransferable(
- column,
- newIndexPattern,
- operationDefinitionMap
+export function isColumnTransferable(
+ column: IndexPatternColumn,
+ newIndexPattern: IndexPattern,
+ layer: IndexPatternLayer
+): boolean {
+ return (
+ operationDefinitionMap[column.operationType].isTransferable(
+ column,
+ newIndexPattern,
+ operationDefinitionMap
+ ) &&
+ (!('references' in column) ||
+ column.references.every((columnId) =>
+ isColumnTransferable(layer.columns[columnId], newIndexPattern, layer)
+ ))
);
}
@@ -1092,15 +1102,7 @@ export function updateLayerIndexPattern(
newIndexPattern: IndexPattern
): IndexPatternLayer {
const keptColumns: IndexPatternLayer['columns'] = _.pickBy(layer.columns, (column) => {
- if ('references' in column) {
- return (
- isColumnTransferable(column, newIndexPattern) &&
- column.references.every((columnId) =>
- isColumnTransferable(layer.columns[columnId], newIndexPattern)
- )
- );
- }
- return isColumnTransferable(column, newIndexPattern);
+ return isColumnTransferable(column, newIndexPattern, layer);
});
const newColumns: IndexPatternLayer['columns'] = _.mapValues(keptColumns, (column) => {
const operationDefinition = operationDefinitionMap[column.operationType];
From 571501a26592ff34aab397e4190c4f66a0502d65 Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 10 May 2021 10:35:12 +0200
Subject: [PATCH 2/3] move main column generation into parse module
---
.../formula/editor/formula_editor.tsx | 3 +-
.../definitions/formula/formula.tsx | 66 +------------------
.../operations/definitions/formula/parse.ts | 62 ++++++++++++++++-
3 files changed, 66 insertions(+), 65 deletions(-)
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
index 7b96aec4194a4..42f4d9cf6ca33 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
@@ -31,7 +31,8 @@ import { LANGUAGE_ID } from './math_tokenization';
import { MemoizedFormulaHelp } from './formula_help';
import './formula.scss';
-import { FormulaIndexPatternColumn, regenerateLayerFromAst } from '../formula';
+import { FormulaIndexPatternColumn } from '../formula';
+import { regenerateLayerFromAst } from '../parse';
export function FormulaEditor({
layer,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx
index 6f0abe8f55568..6494c47548f2f 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx
@@ -6,14 +6,12 @@
*/
import { i18n } from '@kbn/i18n';
-import type { TinymathLocation } from '@kbn/tinymath';
-import { OperationDefinition, GenericOperationDefinition } from '../index';
+import { OperationDefinition } from '../index';
import { ReferenceBasedIndexPatternColumn } from '../column_types';
-import { IndexPattern, IndexPatternLayer } from '../../../types';
-import { getColumnOrder } from '../../layer_helpers';
+import { IndexPattern } from '../../../types';
import { runASTValidation, tryToParse } from './validation';
import { FormulaEditor } from './editor';
-import { parseAndExtract } from './parse';
+import { regenerateLayerFromAst } from './parse';
import { generateFormula } from './generate';
const defaultLabel = i18n.translate('xpack.lens.indexPattern.formulaLabel', {
@@ -158,61 +156,3 @@ export const formulaOperation: OperationDefinition<
paramEditor: FormulaEditor,
};
-
-export function regenerateLayerFromAst(
- text: string,
- layer: IndexPatternLayer,
- columnId: string,
- currentColumn: FormulaIndexPatternColumn,
- indexPattern: IndexPattern,
- operationDefinitionMap: Record
-) {
- const { extracted, isValid } = parseAndExtract(
- text,
- layer,
- columnId,
- indexPattern,
- operationDefinitionMap
- );
-
- const columns = { ...layer.columns };
-
- const locations: Record = {};
-
- Object.keys(columns).forEach((k) => {
- if (k.startsWith(columnId)) {
- delete columns[k];
- }
- });
-
- extracted.forEach(({ column, location }, index) => {
- columns[`${columnId}X${index}`] = column;
- if (location) locations[`${columnId}X${index}`] = location;
- });
-
- columns[columnId] = {
- ...currentColumn,
- params: {
- ...currentColumn.params,
- formula: text,
- isFormulaBroken: !isValid,
- },
- references: !isValid ? [] : [`${columnId}X${extracted.length - 1}`],
- };
-
- return {
- newLayer: {
- ...layer,
- columns,
- columnOrder: getColumnOrder({
- ...layer,
- columns,
- }),
- },
- locations,
- };
-
- // TODO
- // turn ast into referenced columns
- // set state
-}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts
index 9ddc1973044f8..70ed2f36dfd1c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts
@@ -13,8 +13,10 @@ import { mathOperation } from './math';
import { documentField } from '../../../document_field';
import { runASTValidation, shouldHaveFieldArgument, tryToParse } from './validation';
import { findVariables, getOperationParams, groupArgsByType } from './util';
+import { FormulaIndexPatternColumn } from './formula';
+import { getColumnOrder } from '../../layer_helpers';
-export function parseAndExtract(
+function parseAndExtract(
text: string,
layer: IndexPatternLayer,
columnId: string,
@@ -148,3 +150,61 @@ function extractColumns(
columns.push({ column: mathColumn });
return columns;
}
+
+export function regenerateLayerFromAst(
+ text: string,
+ layer: IndexPatternLayer,
+ columnId: string,
+ currentColumn: FormulaIndexPatternColumn,
+ indexPattern: IndexPattern,
+ operationDefinitionMap: Record
+) {
+ const { extracted, isValid } = parseAndExtract(
+ text,
+ layer,
+ columnId,
+ indexPattern,
+ operationDefinitionMap
+ );
+
+ const columns = { ...layer.columns };
+
+ const locations: Record = {};
+
+ Object.keys(columns).forEach((k) => {
+ if (k.startsWith(columnId)) {
+ delete columns[k];
+ }
+ });
+
+ extracted.forEach(({ column, location }, index) => {
+ columns[`${columnId}X${index}`] = column;
+ if (location) locations[`${columnId}X${index}`] = location;
+ });
+
+ columns[columnId] = {
+ ...currentColumn,
+ params: {
+ ...currentColumn.params,
+ formula: text,
+ isFormulaBroken: !isValid,
+ },
+ references: !isValid ? [] : [`${columnId}X${extracted.length - 1}`],
+ };
+
+ return {
+ newLayer: {
+ ...layer,
+ columns,
+ columnOrder: getColumnOrder({
+ ...layer,
+ columns,
+ }),
+ },
+ locations,
+ };
+
+ // TODO
+ // turn ast into referenced columns
+ // set state
+}
From 70a6b86c86e82f255959d51be70f60803a9678c2 Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 10 May 2021 10:49:08 +0200
Subject: [PATCH 3/3] fix tests
---
.../formula/editor/math_completion.test.ts | 12 ++++++------
.../operations/definitions/formula/formula.test.tsx | 3 ++-
2 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
index 9b5e77b7b90db..9e29160b6747b 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
@@ -6,12 +6,12 @@
*/
import { monaco } from '@kbn/monaco';
-import { createMockedIndexPattern } from '../../../mocks';
-import { GenericOperationDefinition } from '../index';
-import type { IndexPatternField } from '../../../types';
-import type { OperationMetadata } from '../../../../types';
-import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks';
-import { tinymathFunctions } from './util';
+import { createMockedIndexPattern } from '../../../../mocks';
+import { GenericOperationDefinition } from '../../index';
+import type { IndexPatternField } from '../../../../types';
+import type { OperationMetadata } from '../../../../../types';
+import { dataPluginMock } from '../../../../../../../../../src/plugins/data/public/mocks';
+import { tinymathFunctions } from '../util';
import { getSignatureHelp, getHover, suggest } from './math_completion';
const buildGenericColumn = (type: string) => {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
index 433e21eb13345..ce7b48aa1875e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
@@ -7,7 +7,8 @@
import { createMockedIndexPattern } from '../../../mocks';
import { formulaOperation, GenericOperationDefinition, IndexPatternColumn } from '../index';
-import { FormulaIndexPatternColumn, regenerateLayerFromAst } from './formula';
+import { FormulaIndexPatternColumn } from './formula';
+import { regenerateLayerFromAst } from './parse';
import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../../types';
import { tinymathFunctions } from './util';