From bd87d98fbdeabe93ac5a2a020a20cf68b36b938f Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 10 May 2021 10:29:44 +0200
Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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';
From 40c43e412110b2b3f609bc2d8caabb3702079f9b Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 10 May 2021 15:54:47 +0200
Subject: [PATCH 04/11] documentation
---
.../definitions/calculations/counter_rate.tsx | 21 +++
.../calculations/cumulative_sum.tsx | 20 +++
.../definitions/calculations/differences.tsx | 20 +++
.../calculations/moving_average.tsx | 21 +++
.../operations/definitions/cardinality.tsx | 19 +++
.../operations/definitions/count.tsx | 19 +++
.../formula/editor/formula_editor.tsx | 2 +
.../formula/editor/formula_help.tsx | 133 +++++++++++-------
.../operations/definitions/formula/util.ts | 95 +++++++++----
.../operations/definitions/index.ts | 4 +
.../operations/definitions/last_value.tsx | 19 +++
.../operations/definitions/metrics.tsx | 23 +++
.../operations/definitions/percentile.tsx | 17 +++
13 files changed, 332 insertions(+), 81 deletions(-)
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx
index 97582be2f32d6..3fb1367154101 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx
@@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
+import React from 'react';
import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '../column_types';
import { IndexPatternLayer } from '../../../types';
import {
@@ -18,6 +19,7 @@ import {
import { DEFAULT_TIME_SCALE } from '../../time_scale_utils';
import { OperationDefinition } from '..';
import { getFormatFromPreviousColumn } from '../helpers';
+import { Markdown } from '../../../../../../../../src/plugins/kibana_react/public';
const ofName = buildLabelFunction((name?: string) => {
return i18n.translate('xpack.lens.indexPattern.CounterRateOf', {
@@ -126,4 +128,23 @@ export const counterRateOperation: OperationDefinition<
},
timeScalingMode: 'mandatory',
filterable: true,
+ documentation: {
+ section: 'calculation',
+ description: (
+
+ ),
+ },
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx
index e6f4f589f6189..f28f44f0c0daf 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx
@@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
+import React from 'react';
import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '../column_types';
import { IndexPatternLayer } from '../../../types';
import {
@@ -16,6 +17,7 @@ import {
} from './utils';
import { OperationDefinition } from '..';
import { getFormatFromPreviousColumn } from '../helpers';
+import { Markdown } from '../../../../../../../../src/plugins/kibana_react/public';
const ofName = (name?: string) => {
return i18n.translate('xpack.lens.indexPattern.cumulativeSumOf', {
@@ -119,4 +121,22 @@ export const cumulativeSumOperation: OperationDefinition<
)?.join(', ');
},
filterable: true,
+ documentation: {
+ section: 'calculation',
+ description: (
+
+ ),
+ },
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx
index b030e604ada06..84222cf125938 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx
@@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
+import React from 'react';
import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '../column_types';
import { IndexPatternLayer } from '../../../types';
import {
@@ -18,6 +19,7 @@ import {
import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils';
import { OperationDefinition } from '..';
import { getFormatFromPreviousColumn } from '../helpers';
+import { Markdown } from '../../../../../../../../src/plugins/kibana_react/public';
const OPERATION_NAME = 'differences';
@@ -116,4 +118,22 @@ export const derivativeOperation: OperationDefinition<
},
timeScalingMode: 'optional',
filterable: true,
+ documentation: {
+ section: 'calculation',
+ description: (
+
+ ),
+ },
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx
index 88af8e9b6378e..0d2c071ee7ecb 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx
@@ -23,6 +23,7 @@ import { getFormatFromPreviousColumn, isValidNumber, useDebounceWithOptions } fr
import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils';
import { HelpPopover, HelpPopoverButton } from '../../../help_popover';
import type { OperationDefinition, ParamEditorProps } from '..';
+import { Markdown } from '../../../../../../../../src/plugins/kibana_react/public';
const ofName = buildLabelFunction((name?: string) => {
return i18n.translate('xpack.lens.indexPattern.movingAverageOf', {
@@ -136,6 +137,26 @@ export const movingAverageOperation: OperationDefinition<
},
timeScalingMode: 'optional',
filterable: true,
+ documentation: {
+ section: 'calculation',
+ description: (
+
+ ),
+ },
};
function MovingAverageParamEditor({
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx
index df84ecb479de7..63a4b2bd3ad13 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx
@@ -6,10 +6,12 @@
*/
import { i18n } from '@kbn/i18n';
+import React from 'react';
import { AggFunctionsMapping } from '../../../../../../../src/plugins/data/public';
import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public';
import { OperationDefinition } from './index';
import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types';
+import { Markdown } from '../../../../../../../src/plugins/kibana_react/public';
import { getFormatFromPreviousColumn, getInvalidFieldMessage, getSafeName } from './helpers';
@@ -112,4 +114,21 @@ export const cardinalityOperation: OperationDefinition
+ ),
+ },
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx
index d66780a4207e6..d56cb504e9bdf 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx
@@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
+import React from 'react';
import { AggFunctionsMapping } from '../../../../../../../src/plugins/data/public';
import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public';
import { OperationDefinition } from './index';
@@ -16,6 +17,7 @@ import {
adjustTimeScaleLabelSuffix,
adjustTimeScaleOnOtherColumnChange,
} from '../time_scale_utils';
+import { Markdown } from '../../../../../../../src/plugins/kibana_react/public';
const countLabel = i18n.translate('xpack.lens.indexPattern.countOf', {
defaultMessage: 'Count of records',
@@ -97,4 +99,21 @@ export const countOperation: OperationDefinition 500")\`
+ `,
+ })}
+ />
+ ),
+ },
};
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 42f4d9cf6ca33..763e10d198108 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
@@ -512,6 +512,7 @@ export function FormulaEditor({
{isFullscreen ? (
) : (
@@ -533,6 +534,7 @@ export function FormulaEditor({
anchorPosition="leftDown"
>
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
index 1335cfe7e3efa..7bca50eb9c846 100644
--- 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
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
@@ -15,25 +15,37 @@ import {
EuiSelectableOption,
} from '@elastic/eui';
import { Markdown } from '../../../../../../../../../src/plugins/kibana_react/public';
-import { GenericOperationDefinition, ParamEditorProps } from '../../index';
+import { GenericOperationDefinition } from '../../index';
import { IndexPattern } from '../../../../types';
import { tinymathFunctions } from '../util';
import { getPossibleFunctions } from './math_completion';
-import { FormulaIndexPatternColumn } from '../formula';
-
function FormulaHelp({
indexPattern,
operationDefinitionMap,
+ isFullscreen,
}: {
indexPattern: IndexPattern;
operationDefinitionMap: Record;
+ isFullscreen: boolean;
}) {
const [selectedFunction, setSelectedFunction] = useState();
+ const scrollTargets = useRef>({});
+
+ useEffect(() => {
+ if (selectedFunction && scrollTargets.current[selectedFunction]) {
+ scrollTargets.current[selectedFunction].scrollIntoView();
+ }
+ }, [selectedFunction]);
const helpItems: Array = [];
- helpItems.push({ label: 'Math', isGroupLabel: true });
+ helpItems.push({
+ label: i18n.translate('xpack.lens.formulaDocumentation.mathSection', {
+ defaultMessage: 'Math',
+ }),
+ isGroupLabel: true,
+ });
helpItems.push(
...getPossibleFunctions(indexPattern)
@@ -45,15 +57,49 @@ function FormulaHelp({
}))
);
- helpItems.push({ label: 'Elasticsearch', isGroupLabel: true });
+ helpItems.push({
+ label: i18n.translate('xpack.lens.formulaDocumentation.elasticsearchSection', {
+ defaultMessage: 'Elasticsearch',
+ }),
+ isGroupLabel: true,
+ });
// Es aggs
helpItems.push(
...getPossibleFunctions(indexPattern)
- .filter((key) => key in operationDefinitionMap)
+ .filter(
+ (key) =>
+ key in operationDefinitionMap &&
+ operationDefinitionMap[key].documentation?.section === 'elasticsearch'
+ )
+ .map((key) => ({
+ label: `${key}: ${operationDefinitionMap[key].displayName}`,
+ description: operationDefinitionMap[key].documentation?.description,
+ checked:
+ selectedFunction === `${key}: ${operationDefinitionMap[key].displayName}`
+ ? ('on' as const)
+ : undefined,
+ }))
+ );
+
+ helpItems.push({
+ label: i18n.translate('xpack.lens.formulaDocumentation.columnCalculationSection', {
+ defaultMessage: 'Column-wise calculation',
+ }),
+ isGroupLabel: true,
+ });
+
+ // Calculations aggs
+ helpItems.push(
+ ...getPossibleFunctions(indexPattern)
+ .filter(
+ (key) =>
+ key in operationDefinitionMap &&
+ operationDefinitionMap[key].documentation?.section === 'calculation'
+ )
.map((key) => ({
label: `${key}: ${operationDefinitionMap[key].displayName}`,
- description: getHelpText(key, operationDefinitionMap),
+ description: operationDefinitionMap[key].documentation?.description,
checked:
selectedFunction === `${key}: ${operationDefinitionMap[key].displayName}`
? ('on' as const)
@@ -62,7 +108,7 @@ function FormulaHelp({
);
return (
-
+
- {selectedFunction ? (
- helpItems.find(({ label }) => label === selectedFunction)?.description
- ) : (
-
- )}
+ description:
+ 'Text is in markdown. Do not translate function names or field names like sum(bytes)',
+ })}
+ />
+ {helpItems.map((item, index) => {
+ if (item.isGroupLabel) {
+ return null;
+ } else {
+ return (
+ {
+ if (el) {
+ scrollTargets.current[item.label] = el;
+ }
+ }}
+ >
+ {item.description}
+
+ );
+ }
+ })}
@@ -139,37 +198,3 @@ Use the symbols +, -, /, and * to perform basic math.
}
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/util.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts
index 5d9a8647eb7ab..6b083e5959378 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
@@ -85,9 +85,13 @@ export const tinymathFunctions: Record<
{ name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) },
],
help: `
+# add \`+\`
+Adds up two numbers.
Also works with + symbol
-Example: ${'`count() + sum(bytes)`'}
-Example: ${'`add(count(), 5)`'}
+
+Example: Calculate the sum of two fields \`sum(price) + sum(tax)\`
+
+Example: Offset count by a static value \`add(count(), 5)\`
`,
},
subtract: {
@@ -96,8 +100,11 @@ Example: ${'`add(count(), 5)`'}
{ name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) },
],
help: `
+# subtract \`-\`
+Subtracts the first number from the second number.
Also works with ${'`-`'} symbol
-Example: ${'`subtract(sum(bytes), avg(bytes))`'}
+
+Example: Calculate the range of a field ${'`subtract(max(bytes), min(bytes))`'}
`,
},
multiply: {
@@ -106,8 +113,13 @@ Example: ${'`subtract(sum(bytes), avg(bytes))`'}
{ name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) },
],
help: `
-Also works with ${'`*`'} symbol
-Example: ${'`multiply(sum(bytes), 2)`'}
+# multiply \`*\`
+Multiplies two numbers.
+Also works with ${'`*`'} symbol.
+
+Example: Calculate price after current tax rate ${'`sum(bytes) * last_value(tax_rate)`'}
+
+Example: Calculate price after constant tax rate \`multiply(sum(price), 1.2)\`
`,
},
divide: {
@@ -116,8 +128,11 @@ Example: ${'`multiply(sum(bytes), 2)`'}
{ name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) },
],
help: `
+# divide \`/\`
+Divides the first number by the second number.
Also works with ${'`/`'} symbol
-Example: ${'`ceil(sum(bytes))`'}
+
+Example: Calculate profit margin \`sum(profit) / sum(revenue)\`
`,
},
abs: {
@@ -125,8 +140,10 @@ Example: ${'`ceil(sum(bytes))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-Absolute value
-Example: ${'`abs(sum(bytes))`'}
+# abs
+Calculates absolute value. A negative value is multiplied by -1, a positive value stays the same.
+
+Example: Calculate average distance to sea level ${'`abs(average(altitude))`'}
`,
},
cbrt: {
@@ -134,8 +151,10 @@ Example: ${'`abs(sum(bytes))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-Cube root of value
-Example: ${'`cbrt(sum(bytes))`'}
+# cbrt
+Cube root of value.
+
+Example: Calculate side length from volume ${'`cbrt(last_value(volume))`'}
`,
},
ceil: {
@@ -143,8 +162,10 @@ Example: ${'`cbrt(sum(bytes))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-Ceiling of value, rounds up
-Example: ${'`ceil(sum(bytes))`'}
+# ceil
+Ceiling of value, rounds up.
+
+Example: Round up price to the next dollar ${'`ceil(sum(price))`'}
`,
},
clamp: {
@@ -154,8 +175,10 @@ Example: ${'`ceil(sum(bytes))`'}
{ name: i18n.translate('xpack.lens.formula.max', { defaultMessage: 'max' }) },
],
help: `
-Limits the value from a minimum to maximum
-Example: ${'`ceil(sum(bytes))`'}
+# clamp
+Limits the value from a minimum to maximum.
+
+Example: Make sure to catch outliers ${'`clamp(average(bytes), percentile(bytes, percentile=5), percentile(bytes, percentile=95))`'}
`,
},
cube: {
@@ -163,8 +186,10 @@ Example: ${'`ceil(sum(bytes))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-Limits the value from a minimum to maximum
-Example: ${'`ceil(sum(bytes))`'}
+# cube
+Calculates the cube of a number.
+
+Example: Calculate volume from side length ${'`cube(last_value(length))`'}
`,
},
exp: {
@@ -172,8 +197,10 @@ Example: ${'`ceil(sum(bytes))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
+# exp
Raises e to the nth power.
-Example: ${'`exp(sum(bytes))`'}
+
+Example: Calculate the natural expontential function ${'`exp(last_value(duration))`'}
`,
},
fix: {
@@ -181,8 +208,10 @@ Example: ${'`exp(sum(bytes))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
+# fix
For positive values, takes the floor. For negative values, takes the ceiling.
-Example: ${'`fix(sum(bytes))`'}
+
+Example: Rounding towards zero ${'`fix(sum(profit))`'}
`,
},
floor: {
@@ -190,8 +219,10 @@ Example: ${'`fix(sum(bytes))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
+# floor
Round down to nearest integer value
-Example: ${'`floor(sum(bytes))`'}
+
+Example: Round down a price ${'`floor(sum(price))`'}
`,
},
log: {
@@ -203,9 +234,10 @@ Example: ${'`floor(sum(bytes))`'}
},
],
help: `
+# log
Logarithm with optional base. The natural base e is used as default.
-Example: ${'`log(sum(bytes))`'}
-Example: ${'`log(sum(bytes), 2)`'}
+
+Example: Calculate number of bits required to store values ${'`log(max(price), 2)`'}
`,
},
// TODO: check if this is valid for Tinymath
@@ -227,8 +259,10 @@ Example: ${'`log(sum(bytes), 2)`'}
},
],
help: `
+# mod
Remainder after dividing the function by a number
-Example: ${'`mod(sum(bytes), 2)`'}
+
+Example: Calculate last three digits of a value ${'`mod(sum(price), 1000)`'}
`,
},
pow: {
@@ -239,8 +273,10 @@ Example: ${'`mod(sum(bytes), 2)`'}
},
],
help: `
+# pow
Raises the value to a certain power. The second argument is required
-Example: ${'`pow(sum(bytes), 3)`'}
+
+Example: Calculate volume based on side length ${'`pow(last_value(length), 3)`'}
`,
},
round: {
@@ -252,9 +288,10 @@ Example: ${'`pow(sum(bytes), 3)`'}
},
],
help: `
+# round
Rounds to a specific number of decimal places, default of 0
-Example: ${'`round(sum(bytes))`'}
-Example: ${'`round(sum(bytes), 2)`'}
+
+Example: Round to the cent ${'`round(sum(price), 2)`'}
`,
},
sqrt: {
@@ -262,8 +299,10 @@ Example: ${'`round(sum(bytes), 2)`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
+# sqrt
Square root of a positive value only
-Example: ${'`sqrt(sum(bytes))`'}
+
+Example: Calculate side length based on area ${'`sqrt(last_value(area))`'}
`,
},
square: {
@@ -271,8 +310,10 @@ Example: ${'`sqrt(sum(bytes))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
+# square
Raise the value to the 2nd power
-Example: ${'`square(sum(bytes))`'}
+
+Example: Calculate area based on side length ${'`square(last_value(length))`'}
`,
},
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts
index 27982243f8c2b..510a59b109d10 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts
@@ -262,6 +262,10 @@ interface BaseOperationDefinitionProps {
* Operations can be used as middleware for other operations, hence not shown in the panel UI
*/
hidden?: boolean;
+ documentation?: {
+ description: JSX.Element;
+ section: 'elasticsearch' | 'calculation';
+ };
}
interface BaseBuildColumnArgs {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx
index a61cca89dfecf..78aace978bfcf 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx
@@ -16,6 +16,7 @@ import { IndexPatternField, IndexPattern } from '../../types';
import { updateColumnParam } from '../layer_helpers';
import { DataType } from '../../../types';
import { getFormatFromPreviousColumn, getInvalidFieldMessage, getSafeName } from './helpers';
+import { Markdown } from '../../../../../../../src/plugins/kibana_react/public';
function ofName(name: string) {
return i18n.translate('xpack.lens.indexPattern.lastValueOf', {
@@ -268,4 +269,22 @@ export const lastValueOperation: OperationDefinition
);
},
+ documentation: {
+ section: 'elasticsearch',
+ description: (
+
+ ),
+ },
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx
index 866d232aab5b3..f1bfb8ccbada6 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx
@@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
+import React from 'react';
import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public';
import { OperationDefinition } from './index';
import { getFormatFromPreviousColumn, getInvalidFieldMessage, getSafeName } from './helpers';
@@ -18,6 +19,7 @@ import {
adjustTimeScaleLabelSuffix,
adjustTimeScaleOnOtherColumnChange,
} from '../time_scale_utils';
+import { Markdown } from '../../../../../../../src/plugins/kibana_react/public';
type MetricColumn = FormattedIndexPatternColumn &
FieldBasedIndexPatternColumn & {
@@ -128,6 +130,27 @@ function buildMetricOperation>({
getErrorMessage: (layer, columnId, indexPattern) =>
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
filterable: true,
+ documentation: {
+ section: 'elasticsearch',
+ description: (
+
+ ),
+ },
} as OperationDefinition;
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx
index 187dc2dc53ffb..2d621f9baab7d 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx
@@ -19,6 +19,7 @@ import {
useDebounceWithOptions,
} from './helpers';
import { FieldBasedIndexPatternColumn } from './column_types';
+import { Markdown } from '../../../../../../../src/plugins/kibana_react/public';
export interface PercentileIndexPatternColumn extends FieldBasedIndexPatternColumn {
operationType: 'percentile';
@@ -192,4 +193,20 @@ export const percentileOperation: OperationDefinition
);
},
+ documentation: {
+ section: 'elasticsearch',
+ description: (
+
+ ),
+ },
};
From 091ca4384af29bad6772280e573fed91711f656c Mon Sep 17 00:00:00 2001
From: Constance
Date: Fri, 14 May 2021 15:13:26 -0700
Subject: [PATCH 05/11] [App Search] Meta engines schema view (#100087)
* Set up TruncatedEnginesList component
- Used for listing source engines
- New in Kibana: now links to source engine schema pages for easier schema fixes!
* Add meta engines schema active fields table
* Render meta engine schema conflicts table & warning callout
* Update x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx
Co-authored-by: Jason Stoltzfus
Co-authored-by: Jason Stoltzfus
---
.../components/schema/components/index.ts | 3 +
.../meta_engines_conflicts_table.test.tsx | 69 ++++++++++++++++
.../meta_engines_conflicts_table.tsx | 69 ++++++++++++++++
.../meta_engines_schema_table.test.tsx | 63 +++++++++++++++
.../components/meta_engines_schema_table.tsx | 78 +++++++++++++++++++
.../truncated_engines_list.test.tsx | 41 ++++++++++
.../components/truncated_engines_list.tsx | 60 ++++++++++++++
.../schema/views/meta_engine_schema.test.tsx | 15 +++-
.../schema/views/meta_engine_schema.tsx | 76 +++++++++++++++++-
9 files changed, 469 insertions(+), 5 deletions(-)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts
index 7da44849b5bc0..6e17547a93980 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts
@@ -8,3 +8,6 @@
export { SchemaCallouts } from './schema_callouts';
export { SchemaTable } from './schema_table';
export { EmptyState } from './empty_state';
+export { MetaEnginesSchemaTable } from './meta_engines_schema_table';
+export { MetaEnginesConflictsTable } from './meta_engines_conflicts_table';
+export { TruncatedEnginesList } from './truncated_engines_list';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx
new file mode 100644
index 0000000000000..eb40d70e13ff8
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx
@@ -0,0 +1,69 @@
+/*
+ * 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 { setMockValues } from '../../../../__mocks__';
+
+import React from 'react';
+
+import { mount } from 'enzyme';
+
+import { EuiTable, EuiTableHeaderCell, EuiTableRow } from '@elastic/eui';
+
+import { MetaEnginesConflictsTable } from './';
+
+describe('MetaEnginesConflictsTable', () => {
+ const values = {
+ conflictingFields: {
+ hello_field: {
+ text: ['engine1'],
+ number: ['engine2'],
+ date: ['engine3'],
+ },
+ world_field: {
+ text: ['engine1'],
+ location: ['engine2', 'engine3', 'engine4'],
+ },
+ },
+ };
+
+ setMockValues(values);
+ const wrapper = mount();
+ const fieldNames = wrapper.find('EuiTableRowCell[data-test-subj="fieldName"]');
+ const fieldTypes = wrapper.find('EuiTableRowCell[data-test-subj="fieldTypes"]');
+ const engines = wrapper.find('EuiTableRowCell[data-test-subj="enginesPerFieldType"]');
+
+ it('renders', () => {
+ expect(wrapper.find(EuiTable)).toHaveLength(1);
+ expect(wrapper.find(EuiTableHeaderCell).at(0).text()).toEqual('Field name');
+ expect(wrapper.find(EuiTableHeaderCell).at(1).text()).toEqual('Field type conflicts');
+ expect(wrapper.find(EuiTableHeaderCell).at(2).text()).toEqual('Engines');
+ });
+
+ it('renders a rowspan on the initial field name column so that it stretches to all associated field conflict rows', () => {
+ expect(fieldNames).toHaveLength(2);
+ expect(fieldNames.at(0).prop('rowSpan')).toEqual(3);
+ expect(fieldNames.at(1).prop('rowSpan')).toEqual(2);
+ });
+
+ it('renders a row for each field type conflict and the engines that have that field type', () => {
+ expect(wrapper.find(EuiTableRow)).toHaveLength(5);
+
+ expect(fieldNames.at(0).text()).toEqual('hello_field');
+ expect(fieldTypes.at(0).text()).toEqual('text');
+ expect(engines.at(0).text()).toEqual('engine1');
+ expect(fieldTypes.at(1).text()).toEqual('number');
+ expect(engines.at(1).text()).toEqual('engine2');
+ expect(fieldTypes.at(2).text()).toEqual('date');
+ expect(engines.at(2).text()).toEqual('engine3');
+
+ expect(fieldNames.at(1).text()).toEqual('world_field');
+ expect(fieldTypes.at(3).text()).toEqual('text');
+ expect(engines.at(3).text()).toEqual('engine1');
+ expect(fieldTypes.at(4).text()).toEqual('location');
+ expect(engines.at(4).text()).toEqual('engine2, engine3, +1');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx
new file mode 100644
index 0000000000000..a37caafe69a59
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx
@@ -0,0 +1,69 @@
+/*
+ * 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 from 'react';
+
+import { useValues } from 'kea';
+
+import {
+ EuiTable,
+ EuiTableHeader,
+ EuiTableHeaderCell,
+ EuiTableBody,
+ EuiTableRow,
+ EuiTableRowCell,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { FIELD_NAME } from '../../../../shared/schema/constants';
+import { ENGINES_TITLE } from '../../engines';
+
+import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic';
+
+import { TruncatedEnginesList } from './';
+
+export const MetaEnginesConflictsTable: React.FC = () => {
+ const { conflictingFields } = useValues(MetaEngineSchemaLogic);
+
+ return (
+
+
+ {FIELD_NAME}
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.fieldTypeConflicts',
+ { defaultMessage: 'Field type conflicts' }
+ )}
+
+ {ENGINES_TITLE}
+
+
+ {Object.entries(conflictingFields).map(([fieldName, conflicts]) =>
+ Object.entries(conflicts).map(([fieldType, engines], i) => {
+ const isFirstRow = i === 0;
+ return (
+
+ {isFirstRow && (
+
+ {fieldName}
+
+ )}
+ {fieldType}
+
+
+
+
+ );
+ })
+ )}
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx
new file mode 100644
index 0000000000000..7d377d5a92714
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 { setMockValues } from '../../../../__mocks__';
+
+import React from 'react';
+
+import { mount } from 'enzyme';
+
+import { EuiTable, EuiTableHeaderCell, EuiTableRow, EuiTableRowCell } from '@elastic/eui';
+
+import { MetaEnginesSchemaTable } from './';
+
+describe('MetaEnginesSchemaTable', () => {
+ const values = {
+ schema: {
+ some_text_field: 'text',
+ some_number_field: 'number',
+ },
+ fields: {
+ some_text_field: {
+ text: ['engine1', 'engine2'],
+ },
+ some_number_field: {
+ number: ['engine1', 'engine2', 'engine3', 'engine4'],
+ },
+ },
+ };
+
+ setMockValues(values);
+ const wrapper = mount();
+ const fieldNames = wrapper.find('EuiTableRowCell[data-test-subj="fieldName"]');
+ const engines = wrapper.find('EuiTableRowCell[data-test-subj="engines"]');
+ const fieldTypes = wrapper.find('EuiTableRowCell[data-test-subj="fieldType"]');
+
+ it('renders', () => {
+ expect(wrapper.find(EuiTable)).toHaveLength(1);
+ expect(wrapper.find(EuiTableHeaderCell).at(0).text()).toEqual('Field name');
+ expect(wrapper.find(EuiTableHeaderCell).at(1).text()).toEqual('Engines');
+ expect(wrapper.find(EuiTableHeaderCell).at(2).text()).toEqual('Field type');
+ });
+
+ it('always renders an initial ID row', () => {
+ expect(wrapper.find('code').at(0).text()).toEqual('id');
+ expect(wrapper.find(EuiTableRowCell).at(1).text()).toEqual('All');
+ });
+
+ it('renders subsequent table rows for each schema field', () => {
+ expect(wrapper.find(EuiTableRow)).toHaveLength(3);
+
+ expect(fieldNames.at(0).text()).toEqual('some_text_field');
+ expect(engines.at(0).text()).toEqual('engine1, engine2');
+ expect(fieldTypes.at(0).text()).toEqual('text');
+
+ expect(fieldNames.at(1).text()).toEqual('some_number_field');
+ expect(engines.at(1).text()).toEqual('engine1, engine2, engine3, +1');
+ expect(fieldTypes.at(1).text()).toEqual('number');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx
new file mode 100644
index 0000000000000..2367ad4e0c53e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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 from 'react';
+
+import { useValues } from 'kea';
+
+import {
+ EuiTable,
+ EuiTableHeader,
+ EuiTableHeaderCell,
+ EuiTableBody,
+ EuiTableRow,
+ EuiTableRowCell,
+ EuiText,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { FIELD_NAME, FIELD_TYPE } from '../../../../shared/schema/constants';
+import { ENGINES_TITLE } from '../../engines';
+
+import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic';
+
+import { TruncatedEnginesList } from './';
+
+export const MetaEnginesSchemaTable: React.FC = () => {
+ const { schema, fields } = useValues(MetaEngineSchemaLogic);
+
+ return (
+
+
+ {FIELD_NAME}
+ {ENGINES_TITLE}
+ {FIELD_TYPE}
+
+
+
+
+
+ id
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.allEngines',
+ { defaultMessage: 'All' }
+ )}
+
+
+
+
+ {Object.keys(fields).map((fieldName) => {
+ const fieldType = schema[fieldName];
+ const engines = fields[fieldName][fieldType];
+
+ return (
+
+
+ {fieldName}
+
+
+
+
+
+ {fieldType}
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx
new file mode 100644
index 0000000000000..193d727be00b5
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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 from 'react';
+
+import { shallow } from 'enzyme';
+
+import { TruncatedEnginesList } from './';
+
+describe('TruncatedEnginesList', () => {
+ it('renders a list of engines with links to their schema pages', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find('[data-test-subj="displayedEngine"]')).toHaveLength(3);
+ expect(wrapper.find('[data-test-subj="displayedEngine"]').first().prop('to')).toEqual(
+ '/engines/engine1/schema'
+ );
+ });
+
+ it('renders a tooltip when the number of engines is greater than the cutoff', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="displayedEngine"]')).toHaveLength(1);
+ expect(wrapper.find('[data-test-subj="hiddenEnginesTooltip"]')).toHaveLength(1);
+ expect(wrapper.find('[data-test-subj="hiddenEnginesTooltip"]').prop('content')).toEqual(
+ 'engine2, engine3'
+ );
+ });
+
+ it('does not render if no engines are passed', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.isEmptyRender()).toBe(true);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx
new file mode 100644
index 0000000000000..a642eb99e3563
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx
@@ -0,0 +1,60 @@
+/*
+ * 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, { Fragment } from 'react';
+
+import { EuiText, EuiToolTip, EuiButtonEmpty } from '@elastic/eui';
+
+import { EuiLinkTo } from '../../../../shared/react_router_helpers';
+import { ENGINE_SCHEMA_PATH } from '../../../routes';
+import { generateEncodedPath } from '../../../utils/encode_path_params';
+
+interface Props {
+ engines?: string[];
+ cutoff?: number;
+}
+
+export const TruncatedEnginesList: React.FC = ({ engines, cutoff = 3 }) => {
+ if (!engines?.length) return null;
+
+ const displayedEngines = engines.slice(0, cutoff);
+ const hiddenEngines = engines.slice(cutoff);
+ const SEPARATOR = ', ';
+
+ return (
+
+ {displayedEngines.map((engineName, i) => {
+ const isLast = i === displayedEngines.length - 1;
+ return (
+
+
+ {engineName}
+
+ {!isLast ? SEPARATOR : ''}
+
+ );
+ })}
+ {hiddenEngines.length > 0 && (
+ <>
+ {SEPARATOR}
+
+
+ +{hiddenEngines.length}
+
+
+ >
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx
index a6e9eef8efa70..b1322c148b577 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx
@@ -12,8 +12,12 @@ import React from 'react';
import { shallow } from 'enzyme';
+import { EuiCallOut } from '@elastic/eui';
+
import { Loading } from '../../../../shared/loading';
+import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components';
+
import { MetaEngineSchema } from './';
describe('MetaEngineSchema', () => {
@@ -33,8 +37,7 @@ describe('MetaEngineSchema', () => {
it('renders', () => {
const wrapper = shallow();
- expect(wrapper.isEmptyRender()).toBe(false);
- // TODO: Check for schema components
+ expect(wrapper.find(MetaEnginesSchemaTable)).toHaveLength(1);
});
it('calls loadSchema on mount', () => {
@@ -49,4 +52,12 @@ describe('MetaEngineSchema', () => {
expect(wrapper.find(Loading)).toHaveLength(1);
});
+
+ it('renders an inactive fields callout & table when source engines have schema conflicts', () => {
+ setMockValues({ ...values, hasConflicts: true, conflictingFieldsCount: 5 });
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiCallOut)).toHaveLength(1);
+ expect(wrapper.find(MetaEnginesConflictsTable)).toHaveLength(1);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx
index 234fcdb5a5a50..4c0235cf81129 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx
@@ -9,17 +9,19 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import { EuiPageHeader, EuiPageContentBody } from '@elastic/eui';
+import { EuiPageHeader, EuiPageContentBody, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FlashMessages } from '../../../../shared/flash_messages';
import { Loading } from '../../../../shared/loading';
+import { DataPanel } from '../../data_panel';
+import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components';
import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic';
export const MetaEngineSchema: React.FC = () => {
const { loadSchema } = useActions(MetaEngineSchemaLogic);
- const { dataLoading } = useValues(MetaEngineSchemaLogic);
+ const { dataLoading, hasConflicts, conflictingFieldsCount } = useValues(MetaEngineSchemaLogic);
useEffect(() => {
loadSchema();
@@ -40,7 +42,75 @@ export const MetaEngineSchema: React.FC = () => {
)}
/>
- TODO
+
+ {hasConflicts && (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.conflictsCalloutDescription',
+ {
+ defaultMessage:
+ 'The field(s) have an inconsistent field-type across the source engines that make up this meta engine. Apply a consistent field-type from the source engines to make these fields searchable.',
+ }
+ )}
+
+
+
+ >
+ )}
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle',
+ { defaultMessage: 'Active fields' }
+ )}
+
+ }
+ subtitle={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription',
+ { defaultMessage: 'Fields which belong to one or more engine.' }
+ )}
+ >
+
+
+
+ {hasConflicts && (
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle',
+ { defaultMessage: 'Inactive fields' }
+ )}
+
+ }
+ subtitle={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription',
+ {
+ defaultMessage:
+ 'These fields have type conflicts. To activate these fields, change types in the source engines to match.',
+ }
+ )}
+ >
+
+
+ )}
+
>
);
};
From ca2930c71958737ae5ef269bfb3ce915bb626061 Mon Sep 17 00:00:00 2001
From: Dzmitry Lemechko
Date: Sat, 15 May 2021 00:17:10 +0200
Subject: [PATCH 06/11] [status_page test] use navigateToApp (#100146)
---
.github/CODEOWNERS | 2 --
.../apps/status_page/status_page.ts | 7 +++----
.../functional/page_objects/status_page.ts | 20 +------------------
3 files changed, 4 insertions(+), 25 deletions(-)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index de323128afed1..39daa5780436f 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -203,7 +203,6 @@
/packages/kbn-legacy-logging/ @elastic/kibana-core
/packages/kbn-crypto/ @elastic/kibana-core
/packages/kbn-http-tools/ @elastic/kibana-core
-/src/plugins/status_page/ @elastic/kibana-core
/src/plugins/saved_objects_management/ @elastic/kibana-core
/src/dev/run_check_published_api_changes.ts @elastic/kibana-core
/src/plugins/home/public @elastic/kibana-core
@@ -215,7 +214,6 @@
#CC# /src/plugins/legacy_export/ @elastic/kibana-core
#CC# /src/plugins/xpack_legacy/ @elastic/kibana-core
#CC# /src/plugins/saved_objects/ @elastic/kibana-core
-#CC# /src/plugins/status_page/ @elastic/kibana-core
#CC# /x-pack/plugins/cloud/ @elastic/kibana-core
#CC# /x-pack/plugins/features/ @elastic/kibana-core
#CC# /x-pack/plugins/global_search/ @elastic/kibana-core
diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts
index 55a54245cf832..ecef6225632e9 100644
--- a/x-pack/test/functional/apps/status_page/status_page.ts
+++ b/x-pack/test/functional/apps/status_page/status_page.ts
@@ -12,17 +12,16 @@ export default function statusPageFunctonalTests({
getPageObjects,
}: FtrProviderContext) {
const esArchiver = getService('esArchiver');
- const PageObjects = getPageObjects(['security', 'statusPage', 'home']);
+ const PageObjects = getPageObjects(['security', 'statusPage', 'common']);
- // FLAKY: https://github.com/elastic/kibana/issues/50448
- describe.skip('Status Page', function () {
+ describe('Status Page', function () {
this.tags(['skipCloud', 'includeFirefox']);
before(async () => await esArchiver.load('empty_kibana'));
after(async () => await esArchiver.unload('empty_kibana'));
it('allows user to navigate without authentication', async () => {
await PageObjects.security.forceLogout();
- await PageObjects.statusPage.navigateToPage();
+ await PageObjects.common.navigateToApp('status_page', { shouldLoginIfPrompted: false });
await PageObjects.statusPage.expectStatusPage();
});
});
diff --git a/x-pack/test/functional/page_objects/status_page.ts b/x-pack/test/functional/page_objects/status_page.ts
index 9edaf4dea53f8..ed90aef954770 100644
--- a/x-pack/test/functional/page_objects/status_page.ts
+++ b/x-pack/test/functional/page_objects/status_page.ts
@@ -5,36 +5,18 @@
* 2.0.
*/
-import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export function StatusPagePageProvider({ getService }: FtrProviderContext) {
- const retry = getService('retry');
const log = getService('log');
- const browser = getService('browser');
const find = getService('find');
- const deployment = getService('deployment');
-
class StatusPage {
async initTests() {
log.debug('StatusPage:initTests');
}
- async navigateToPage() {
- return await retry.try(async () => {
- const url = deployment.getHostPort() + '/status';
- log.info(`StatusPage:navigateToPage(): ${url}`);
- await browser.get(url);
- });
- }
-
async expectStatusPage(): Promise {
- return await retry.try(async () => {
- log.debug(`expectStatusPage()`);
- await find.byCssSelector('[data-test-subj="statusPageRoot"]', 20000);
- const url = await browser.getCurrentUrl();
- expect(url).to.contain(`/status`);
- });
+ await find.byCssSelector('[data-test-subj="statusPageRoot"]', 20000);
}
}
From bfe08d25c53e99a122a99d685b741ba9b35a8b08 Mon Sep 17 00:00:00 2001
From: Frank Hassanabad
Date: Fri, 14 May 2021 16:56:08 -0600
Subject: [PATCH 07/11] [Security Solutions] Removes deprecation and more
copied code between security solutions and lists plugin (#100150)
## Summary
* Removes deprecations
* Removes duplicated code
### Checklist
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
---
x-pack/plugins/lists/common/shared_exports.ts | 39 ---
.../autocomplete/field_value_match.tsx | 2 +-
.../autocomplete/field_value_match_any.tsx | 2 +-
.../components/autocomplete/helpers.ts | 3 +-
.../use_field_value_autocomplete.test.ts | 2 +-
.../hooks/use_field_value_autocomplete.ts | 2 +-
.../components/autocomplete/operators.ts | 6 +-
.../components/autocomplete/types.ts | 6 +-
.../builder/entry_renderer.stories.tsx | 5 +-
.../components/builder/entry_renderer.tsx | 8 +-
.../builder/exception_item_renderer.tsx | 3 +-
.../builder/exception_items_renderer.tsx | 16 +-
.../components/builder/helpers.test.ts | 330 ++++++++++++++++-
.../exceptions/components/builder/helpers.ts | 24 +-
.../exceptions/components/builder/reducer.ts | 4 +-
.../exceptions/components/builder/types.ts | 16 +-
.../lists/public/exceptions/transforms.ts | 3 +-
x-pack/plugins/lists/public/shared_exports.ts | 14 +-
.../detection_engine/schemas/types/lists.ts | 2 +-
.../common/detection_engine/utils.test.ts | 3 +-
.../common/detection_engine/utils.ts | 9 +-
.../common/shared_imports.ts | 33 --
.../autocomplete/field_value_match.tsx | 3 +-
.../autocomplete/field_value_match_any.tsx | 3 +-
.../common/components/autocomplete/helpers.ts | 3 +-
.../use_field_value_autocomplete.test.ts | 2 +-
.../hooks/use_field_value_autocomplete.ts | 2 +-
.../components/autocomplete/operators.ts | 5 +-
.../common/components/autocomplete/types.ts | 5 +-
.../exceptions/add_exception_comments.tsx | 2 +-
.../add_exception_modal/index.test.tsx | 5 +-
.../exceptions/add_exception_modal/index.tsx | 3 +-
.../exceptions/edit_exception_modal/index.tsx | 3 +-
.../components/exceptions/helpers.test.tsx | 331 +-----------------
.../common/components/exceptions/helpers.tsx | 194 +---------
.../common/components/exceptions/types.ts | 19 +-
.../exceptions/use_add_exception.test.tsx | 9 +-
...tch_or_create_rule_exception_list.test.tsx | 5 +-
.../exceptions_viewer_header.stories.tsx | 2 +-
.../viewer/exceptions_viewer_header.test.tsx | 3 +-
.../viewer/exceptions_viewer_header.tsx | 2 +-
.../components/exceptions/viewer/helpers.tsx | 10 +-
.../exceptions/viewer/index.test.tsx | 8 +-
.../components/exceptions/viewer/index.tsx | 3 +-
.../components/exceptions/viewer/reducer.ts | 2 +-
.../timeline_actions/alert_context_menu.tsx | 2 +-
.../value_lists_management_modal/form.tsx | 4 +-
.../rules/all/exceptions/columns.tsx | 2 +-
.../rules/all/exceptions/exceptions_table.tsx | 2 +-
.../detection_engine/rules/create/helpers.ts | 3 +-
.../detection_engine/rules/details/index.tsx | 4 +-
.../pages/event_filters/constants.ts | 3 +-
.../public/shared_imports.ts | 10 +-
.../endpoint/routes/trusted_apps/mapping.ts | 12 +-
.../create_field_and_set_tuples.test.ts | 2 +-
.../filters/create_field_and_set_tuples.ts | 2 +-
56 files changed, 501 insertions(+), 701 deletions(-)
diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts
index bc9d0ca8d7b94..f00afb7ac810d 100644
--- a/x-pack/plugins/lists/common/shared_exports.ts
+++ b/x-pack/plugins/lists/common/shared_exports.ts
@@ -5,45 +5,6 @@
* 2.0.
*/
-// TODO: We should remove these and instead directly import them in the security_solution project. This is to get my PR across the line without too many conflicts.
-export {
- CommentsArray,
- Comment,
- CreateComment,
- CreateCommentsArray,
- Entry,
- EntryExists,
- EntryMatch,
- EntryMatchAny,
- EntryMatchWildcard,
- EntryNested,
- EntryList,
- EntriesArray,
- NamespaceType,
- NestedEntriesArray,
- ListOperator as Operator,
- ListOperatorEnum as OperatorEnum,
- ListOperatorTypeEnum as OperatorTypeEnum,
- listOperator as operator,
- ExceptionListTypeEnum,
- ExceptionListType,
- comment,
- exceptionListType,
- entry,
- entriesNested,
- nestedEntryItem,
- entriesMatch,
- entriesMatchAny,
- entriesMatchWildcard,
- entriesExists,
- entriesList,
- namespaceType,
- osType,
- osTypeArray,
- OsTypeArray,
- Type,
-} from '@kbn/securitysolution-io-ts-list-types';
-
export {
ListSchema,
ExceptionListSchema,
diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx
index a0994871808d1..c1776280842c6 100644
--- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx
+++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx
@@ -14,8 +14,8 @@ import {
EuiSuperSelect,
} from '@elastic/eui';
import { uniq } from 'lodash';
+import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
-import { OperatorTypeEnum } from '../../../../common';
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { AutocompleteStart } from '../../../../../../../src/plugins/data/public';
diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx
index 08958f6d99aab..82347f6212442 100644
--- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx
+++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx
@@ -8,8 +8,8 @@
import React, { useCallback, useMemo, useState } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { uniq } from 'lodash';
+import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
-import { OperatorTypeEnum } from '../../../../common';
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { AutocompleteStart } from '../../../../../../../src/plugins/data/public';
diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts
index 4f25bec3b38dc..b982193d1d349 100644
--- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts
+++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts
@@ -7,8 +7,9 @@
import dateMath from '@elastic/datemath';
import { EuiComboBoxOptionOption } from '@elastic/eui';
+import type { Type } from '@kbn/securitysolution-io-ts-list-types';
-import { ListSchema, Type } from '../../../../common';
+import type { ListSchema } from '../../../../common';
import { IFieldType } from '../../../../../../../src/plugins/data/common';
import {
diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts
index 4e3fb2179d786..0335ffa55d2a2 100644
--- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts
@@ -6,10 +6,10 @@
*/
import { act, renderHook } from '@testing-library/react-hooks';
+import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub';
import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
-import { OperatorTypeEnum } from '../../../../../common';
import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks';
import {
diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts
index 6c6198ac55a0f..674bb5e5537d9 100644
--- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts
+++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts
@@ -7,10 +7,10 @@
import { useEffect, useRef, useState } from 'react';
import { debounce } from 'lodash';
+import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { AutocompleteStart } from '../../../../../../../../src/plugins/data/public';
import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common';
-import { OperatorTypeEnum } from '../../../../../common';
interface FuncArgs {
fieldSelected: IFieldType | undefined;
diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts
index 551dfcb61e3ad..83a424d72ec5f 100644
--- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts
+++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts
@@ -6,8 +6,10 @@
*/
import { i18n } from '@kbn/i18n';
-
-import { OperatorEnum, OperatorTypeEnum } from '../../../../common';
+import {
+ ListOperatorEnum as OperatorEnum,
+ ListOperatorTypeEnum as OperatorTypeEnum,
+} from '@kbn/securitysolution-io-ts-list-types';
import { OperatorOption } from './types';
diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts
index 8ea3e8d927d68..76d5b7758007b 100644
--- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts
+++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts
@@ -6,8 +6,10 @@
*/
import { EuiComboBoxOptionOption } from '@elastic/eui';
-
-import { OperatorEnum, OperatorTypeEnum } from '../../../../common';
+import type {
+ ListOperatorEnum as OperatorEnum,
+ ListOperatorTypeEnum as OperatorTypeEnum,
+} from '@kbn/securitysolution-io-ts-list-types';
export interface GetGenericComboBoxPropsReturn {
comboOptions: EuiComboBoxOptionOption[];
diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx
index 5b3730a6deb93..dd67381c30934 100644
--- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx
+++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx
@@ -9,8 +9,11 @@ import { Story } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import React from 'react';
import { HttpStart } from 'kibana/public';
+import {
+ ListOperatorEnum as OperatorEnum,
+ ListOperatorTypeEnum as OperatorTypeEnum,
+} from '@kbn/securitysolution-io-ts-list-types';
-import { OperatorEnum, OperatorTypeEnum } from '../../../../common';
import { AutocompleteStart } from '../../../../../../../src/plugins/data/public';
import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common';
diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx
index 0ece28d409bd5..09863660e98af 100644
--- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx
+++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx
@@ -8,7 +8,11 @@
import React, { useCallback, useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
import styled from 'styled-components';
-import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types';
+import {
+ ExceptionListType,
+ ListOperatorTypeEnum as OperatorTypeEnum,
+ OsTypeArray,
+} from '@kbn/securitysolution-io-ts-list-types';
import { AutocompleteStart } from '../../../../../../../src/plugins/data/public';
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
@@ -21,7 +25,7 @@ import { AutocompleteFieldExistsComponent } from '../autocomplete/field_value_ex
import { AutocompleteFieldMatchComponent } from '../autocomplete/field_value_match';
import { AutocompleteFieldMatchAnyComponent } from '../autocomplete/field_value_match_any';
import { AutocompleteFieldListsComponent } from '../autocomplete/field_value_lists';
-import { ExceptionListType, ListSchema, OperatorTypeEnum } from '../../../../common';
+import { ListSchema } from '../../../../common';
import { getEmptyValue } from '../../../common/empty_value';
import {
diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx
index 94c3bff8f4cf9..e10cd2934328f 100644
--- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx
+++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx
@@ -10,9 +10,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { HttpStart } from 'kibana/public';
import { AutocompleteStart } from 'src/plugins/data/public';
-import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types';
+import { ExceptionListType, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types';
-import { ExceptionListType } from '../../../../common';
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types';
diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx
index 4ec152e155e39..f771969a92025 100644
--- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx
+++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx
@@ -10,19 +10,21 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { HttpStart } from 'kibana/public';
import { addIdToItem } from '@kbn/securitysolution-utils';
-import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types';
-
-import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public';
import {
- CreateExceptionListItemSchema,
- ExceptionListItemSchema,
ExceptionListType,
NamespaceType,
- OperatorEnum,
- OperatorTypeEnum,
+ ListOperatorEnum as OperatorEnum,
+ ListOperatorTypeEnum as OperatorTypeEnum,
+ OsTypeArray,
entriesNested,
+} from '@kbn/securitysolution-io-ts-list-types';
+
+import {
+ CreateExceptionListItemSchema,
+ ExceptionListItemSchema,
exceptionListItemSchema,
} from '../../../../common';
+import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public';
import { AndOrBadge } from '../and_or_badge';
import { CreateExceptionListItemBuilderSchema, ExceptionsBuilderExceptionItem } from './types';
diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts
index 1e74193299e56..dbfeaa4a258ca 100644
--- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts
@@ -5,6 +5,18 @@
* 2.0.
*/
+import {
+ EntryExists,
+ EntryList,
+ EntryMatch,
+ EntryMatchAny,
+ EntryNested,
+ ExceptionListType,
+ ListOperatorEnum as OperatorEnum,
+ ListOperatorTypeEnum as OperatorTypeEnum,
+} from '@kbn/securitysolution-io-ts-list-types';
+
+import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../../../../common';
import { ENTRIES_WITH_IDS } from '../../../../common/constants.mock';
import { getEntryExistsMock } from '../../../../common/schemas/types/entry_exists.mock';
import { getExceptionListItemSchemaMock } from '../../../../common/schemas/response/exception_list_item_schema.mock';
@@ -23,25 +35,23 @@ import {
doesNotExistOperator,
existsOperator,
isInListOperator,
+ isNotInListOperator,
isNotOneOfOperator,
isNotOperator,
isOneOfOperator,
isOperator,
} from '../autocomplete/operators';
-import {
- EntryExists,
- EntryList,
- EntryMatch,
- EntryMatchAny,
- EntryNested,
- ExceptionListType,
- OperatorEnum,
- OperatorTypeEnum,
-} from '../../../../common';
import { OperatorOption } from '../autocomplete/types';
+import { getEntryListMock } from '../../../../common/schemas/types/entry_list.mock';
-import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types';
import {
+ BuilderEntry,
+ EmptyEntry,
+ ExceptionsBuilderExceptionItem,
+ FormattedBuilderEntry,
+} from './types';
+import {
+ filterExceptionItems,
getCorrespondingKeywordField,
getEntryFromOperator,
getEntryOnFieldChange,
@@ -49,10 +59,14 @@ import {
getEntryOnMatchAnyChange,
getEntryOnMatchChange,
getEntryOnOperatorChange,
+ getEntryValue,
+ getExceptionOperatorSelect,
getFilteredIndexPatterns,
getFormattedBuilderEntries,
getFormattedBuilderEntry,
+ getNewExceptionItem,
getOperatorOptions,
+ getOperatorType,
getUpdatedEntriesOnDelete,
isEntryNested,
} from './helpers';
@@ -1426,4 +1440,298 @@ describe('Exception builder helpers', () => {
expect(output).toEqual(undefined);
});
});
+
+ describe('#getOperatorType', () => {
+ test('returns operator type "match" if entry.type is "match"', () => {
+ const payload = getEntryMatchMock();
+ const operatorType = getOperatorType(payload);
+
+ expect(operatorType).toEqual(OperatorTypeEnum.MATCH);
+ });
+
+ test('returns operator type "match_any" if entry.type is "match_any"', () => {
+ const payload = getEntryMatchAnyMock();
+ const operatorType = getOperatorType(payload);
+
+ expect(operatorType).toEqual(OperatorTypeEnum.MATCH_ANY);
+ });
+
+ test('returns operator type "list" if entry.type is "list"', () => {
+ const payload = getEntryListMock();
+ const operatorType = getOperatorType(payload);
+
+ expect(operatorType).toEqual(OperatorTypeEnum.LIST);
+ });
+
+ test('returns operator type "exists" if entry.type is "exists"', () => {
+ const payload = getEntryExistsMock();
+ const operatorType = getOperatorType(payload);
+
+ expect(operatorType).toEqual(OperatorTypeEnum.EXISTS);
+ });
+ });
+
+ describe('#getExceptionOperatorSelect', () => {
+ test('it returns "isOperator" when "operator" is "included" and operator type is "match"', () => {
+ const payload = getEntryMatchMock();
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(isOperator);
+ });
+
+ test('it returns "isNotOperator" when "operator" is "excluded" and operator type is "match"', () => {
+ const payload = getEntryMatchMock();
+ payload.operator = 'excluded';
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(isNotOperator);
+ });
+
+ test('it returns "isOneOfOperator" when "operator" is "included" and operator type is "match_any"', () => {
+ const payload = getEntryMatchAnyMock();
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(isOneOfOperator);
+ });
+
+ test('it returns "isNotOneOfOperator" when "operator" is "excluded" and operator type is "match_any"', () => {
+ const payload = getEntryMatchAnyMock();
+ payload.operator = 'excluded';
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(isNotOneOfOperator);
+ });
+
+ test('it returns "existsOperator" when "operator" is "included" and no operator type is provided', () => {
+ const payload = getEntryExistsMock();
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(existsOperator);
+ });
+
+ test('it returns "doesNotExistsOperator" when "operator" is "excluded" and no operator type is provided', () => {
+ const payload = getEntryExistsMock();
+ payload.operator = 'excluded';
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(doesNotExistOperator);
+ });
+
+ test('it returns "isInList" when "operator" is "included" and operator type is "list"', () => {
+ const payload = getEntryListMock();
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(isInListOperator);
+ });
+
+ test('it returns "isNotInList" when "operator" is "excluded" and operator type is "list"', () => {
+ const payload = getEntryListMock();
+ payload.operator = 'excluded';
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(isNotInListOperator);
+ });
+ });
+
+ describe('#filterExceptionItems', () => {
+ // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes
+ // for context around the temporary `id`
+ test('it correctly validates entries that include a temporary `id`', () => {
+ const output: Array<
+ ExceptionListItemSchema | CreateExceptionListItemSchema
+ > = filterExceptionItems([
+ { ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS },
+ ]);
+
+ expect(output).toEqual([{ ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }]);
+ });
+
+ test('it removes entry items with "value" of "undefined"', () => {
+ const { entries, ...rest } = getExceptionListItemSchemaMock();
+ const mockEmptyException: EmptyEntry = {
+ field: 'host.name',
+ id: '123',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH,
+ value: undefined,
+ };
+ const exceptions = filterExceptionItems([
+ {
+ ...rest,
+ entries: [...entries, mockEmptyException],
+ },
+ ]);
+
+ expect(exceptions).toEqual([getExceptionListItemSchemaMock()]);
+ });
+
+ test('it removes "match" entry items with "value" of empty string', () => {
+ const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
+ const mockEmptyException: EmptyEntry = {
+ field: 'host.name',
+ id: '123',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH,
+ value: '',
+ };
+ const output: Array<
+ ExceptionListItemSchema | CreateExceptionListItemSchema
+ > = filterExceptionItems([
+ {
+ ...rest,
+ entries: [...entries, mockEmptyException],
+ },
+ ]);
+
+ expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
+ });
+
+ test('it removes "match" entry items with "field" of empty string', () => {
+ const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
+ const mockEmptyException: EmptyEntry = {
+ field: '',
+ id: '123',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH,
+ value: 'some value',
+ };
+ const output: Array<
+ ExceptionListItemSchema | CreateExceptionListItemSchema
+ > = filterExceptionItems([
+ {
+ ...rest,
+ entries: [...entries, mockEmptyException],
+ },
+ ]);
+
+ expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
+ });
+
+ test('it removes "match_any" entry items with "field" of empty string', () => {
+ const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
+ const mockEmptyException: EmptyEntry = {
+ field: '',
+ id: '123',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH_ANY,
+ value: ['some value'],
+ };
+ const output: Array<
+ ExceptionListItemSchema | CreateExceptionListItemSchema
+ > = filterExceptionItems([
+ {
+ ...rest,
+ entries: [...entries, mockEmptyException],
+ },
+ ]);
+
+ expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
+ });
+
+ test('it removes "nested" entry items with "field" of empty string', () => {
+ const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
+ const mockEmptyException: EntryNested = {
+ entries: [getEntryMatchMock()],
+ field: '',
+ type: OperatorTypeEnum.NESTED,
+ };
+ const output: Array<
+ ExceptionListItemSchema | CreateExceptionListItemSchema
+ > = filterExceptionItems([
+ {
+ ...rest,
+ entries: [...entries, mockEmptyException],
+ },
+ ]);
+
+ expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
+ });
+
+ test('it removes the "nested" entry entries with "value" of empty string', () => {
+ const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
+ const mockEmptyException: EntryNested = {
+ entries: [getEntryMatchMock(), { ...getEntryMatchMock(), value: '' }],
+ field: 'host.name',
+ type: OperatorTypeEnum.NESTED,
+ };
+ const output: Array<
+ ExceptionListItemSchema | CreateExceptionListItemSchema
+ > = filterExceptionItems([
+ {
+ ...rest,
+ entries: [...entries, mockEmptyException],
+ },
+ ]);
+
+ expect(output).toEqual([
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [
+ ...getExceptionListItemSchemaMock().entries,
+ { ...mockEmptyException, entries: [getEntryMatchMock()] },
+ ],
+ },
+ ]);
+ });
+
+ test('it removes the "nested" entry item if all its entries are invalid', () => {
+ const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
+ const mockEmptyException: EntryNested = {
+ entries: [{ ...getEntryMatchMock(), value: '' }],
+ field: 'host.name',
+ type: OperatorTypeEnum.NESTED,
+ };
+ const output: Array<
+ ExceptionListItemSchema | CreateExceptionListItemSchema
+ > = filterExceptionItems([
+ {
+ ...rest,
+ entries: [...entries, mockEmptyException],
+ },
+ ]);
+
+ expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
+ });
+
+ test('it removes `temporaryId` from items', () => {
+ const { meta, ...rest } = getNewExceptionItem({
+ listId: '123',
+ namespaceType: 'single',
+ ruleName: 'rule name',
+ });
+ const exceptions = filterExceptionItems([{ ...rest, meta }]);
+
+ expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]);
+ });
+ });
+
+ describe('#getEntryValue', () => {
+ it('returns "match" entry value', () => {
+ const payload = getEntryMatchMock();
+ const result = getEntryValue(payload);
+ const expected = 'some host name';
+ expect(result).toEqual(expected);
+ });
+
+ it('returns "match any" entry values', () => {
+ const payload = getEntryMatchAnyMock();
+ const result = getEntryValue(payload);
+ const expected = ['some host name'];
+ expect(result).toEqual(expected);
+ });
+
+ it('returns "exists" entry value', () => {
+ const payload = getEntryExistsMock();
+ const result = getEntryValue(payload);
+ const expected = undefined;
+ expect(result).toEqual(expected);
+ });
+
+ it('returns "list" entry value', () => {
+ const payload = getEntryListMock();
+ const result = getEntryValue(payload);
+ const expected = 'some-list-id';
+ expect(result).toEqual(expected);
+ });
+ });
});
diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts
index 18d607d6807fc..6cd9dec0dc7a1 100644
--- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts
+++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts
@@ -8,27 +8,29 @@
import uuid from 'uuid';
import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils';
import { validate } from '@kbn/securitysolution-io-ts-utils';
-import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types';
-
-import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public';
import {
- CreateExceptionListItemSchema,
EntriesArray,
Entry,
EntryNested,
- ExceptionListItemSchema,
ExceptionListType,
- ListSchema,
NamespaceType,
- OperatorEnum,
- OperatorTypeEnum,
- createExceptionListItemSchema,
+ ListOperatorEnum as OperatorEnum,
+ ListOperatorTypeEnum as OperatorTypeEnum,
+ OsTypeArray,
entriesList,
entriesNested,
entry,
- exceptionListItemSchema,
nestedEntryItem,
+} from '@kbn/securitysolution-io-ts-list-types';
+
+import {
+ CreateExceptionListItemSchema,
+ ExceptionListItemSchema,
+ ListSchema,
+ createExceptionListItemSchema,
+ exceptionListItemSchema,
} from '../../../../common';
+import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public';
import {
EXCEPTION_OPERATORS,
EXCEPTION_OPERATORS_SANS_LISTS,
@@ -96,7 +98,7 @@ export const filterExceptionItems = (
return [...acc, item];
} else if (createExceptionListItemSchema.is(item)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { meta: _, ...rest } = item;
+ const { meta, ...rest } = item;
const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined };
return [...acc, itemSansMetaId];
} else {
diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts b/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts
index 92df2fd3793de..0e8a5fadd3b1a 100644
--- a/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts
+++ b/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts
@@ -5,7 +5,9 @@
* 2.0.
*/
-import { ExceptionListItemSchema, OperatorTypeEnum } from '../../../../common';
+import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
+
+import { ExceptionListItemSchema } from '../../../../common';
import { ExceptionsBuilderExceptionItem } from './types';
import { getDefaultEmptyEntry } from './helpers';
diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/types.ts b/x-pack/plugins/lists/public/exceptions/components/builder/types.ts
index 800f1445217b9..5cf4238ab5e0c 100644
--- a/x-pack/plugins/lists/public/exceptions/components/builder/types.ts
+++ b/x-pack/plugins/lists/public/exceptions/components/builder/types.ts
@@ -5,20 +5,20 @@
* 2.0.
*/
-import { IFieldType } from '../../../../../../../src/plugins/data/common';
-import { OperatorOption } from '../autocomplete/types';
-import {
- CreateExceptionListItemSchema,
+import type {
Entry,
EntryExists,
EntryMatch,
EntryMatchAny,
EntryMatchWildcard,
EntryNested,
- ExceptionListItemSchema,
- OperatorEnum,
- OperatorTypeEnum,
-} from '../../../../common';
+ ListOperatorEnum as OperatorEnum,
+ ListOperatorTypeEnum as OperatorTypeEnum,
+} from '@kbn/securitysolution-io-ts-list-types';
+
+import type { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../../../../common';
+import { IFieldType } from '../../../../../../../src/plugins/data/common';
+import { OperatorOption } from '../autocomplete/types';
export interface FormattedBuilderEntry {
id: string;
diff --git a/x-pack/plugins/lists/public/exceptions/transforms.ts b/x-pack/plugins/lists/public/exceptions/transforms.ts
index 50ce1b6e33a4b..564ba1a699f98 100644
--- a/x-pack/plugins/lists/public/exceptions/transforms.ts
+++ b/x-pack/plugins/lists/public/exceptions/transforms.ts
@@ -7,11 +7,10 @@
import { flow } from 'fp-ts/lib/function';
import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils';
+import type { EntriesArray, Entry } from '@kbn/securitysolution-io-ts-list-types';
import type {
CreateExceptionListItemSchema,
- EntriesArray,
- Entry,
ExceptionListItemSchema,
UpdateExceptionListItemSchema,
} from '../../common';
diff --git a/x-pack/plugins/lists/public/shared_exports.ts b/x-pack/plugins/lists/public/shared_exports.ts
index 2032a44a8fd33..6d14c6b541904 100644
--- a/x-pack/plugins/lists/public/shared_exports.ts
+++ b/x-pack/plugins/lists/public/shared_exports.ts
@@ -7,11 +7,8 @@
// Exports to be shared with plugins
export { withOptionalSignal } from './common/with_optional_signal';
-export { useIsMounted } from './common/hooks/use_is_mounted';
export { useAsync } from './common/hooks/use_async';
export { useApi } from './exceptions/hooks/use_api';
-export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item';
-export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list';
export { useExceptionListItems } from './exceptions/hooks/use_exception_list_items';
export { useExceptionLists } from './exceptions/hooks/use_exception_lists';
export { useFindLists } from './lists/hooks/use_find_lists';
@@ -24,13 +21,18 @@ export { useReadListIndex } from './lists/hooks/use_read_list_index';
export { useCreateListIndex } from './lists/hooks/use_create_list_index';
export { useReadListPrivileges } from './lists/hooks/use_read_list_privileges';
export {
- addExceptionListItem,
- updateExceptionListItem,
+ getEntryValue,
+ getExceptionOperatorSelect,
+ getOperatorType,
+ getNewExceptionItem,
+ addIdToEntries,
+} from './exceptions/components/builder/helpers';
+export {
fetchExceptionListById,
addExceptionList,
addEndpointExceptionList,
} from './exceptions/api';
-export {
+export type {
ExceptionList,
ExceptionListFilter,
ExceptionListIdentifiers,
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts
index 79fd264808138..e2c3ee88f6a65 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts
@@ -7,7 +7,7 @@
import * as t from 'io-ts';
-import { exceptionListType, namespaceType } from '../../../shared_imports';
+import { exceptionListType, namespaceType } from '@kbn/securitysolution-io-ts-list-types';
import { NonEmptyString } from './non_empty_string';
diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
index c477036a07d85..1e0f7e087a5b3 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
@@ -13,7 +13,8 @@ import {
normalizeMachineLearningJobIds,
normalizeThresholdField,
} from './utils';
-import { EntriesArray } from '../shared_imports';
+
+import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types';
describe('#hasLargeValueList', () => {
test('it returns false if empty array', () => {
diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts
index a8e0ffcccef82..611d23fd1ce22 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts
@@ -7,11 +7,10 @@
import { isEmpty } from 'lodash';
-import {
- CreateExceptionListItemSchema,
- EntriesArray,
- ExceptionListItemSchema,
-} from '../shared_imports';
+import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types';
+
+import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../shared_imports';
+
import { Type, JobStatus, Threshold, ThresholdNormalized } from './schemas/common/schemas';
export const hasLargeValueItem = (
diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts
index e987775a8e768..a6bad0347e641 100644
--- a/x-pack/plugins/security_solution/common/shared_imports.ts
+++ b/x-pack/plugins/security_solution/common/shared_imports.ts
@@ -7,44 +7,14 @@
export {
ListSchema,
- CommentsArray,
- CreateCommentsArray,
- Comment,
- CreateComment,
ExceptionListSchema,
ExceptionListItemSchema,
CreateExceptionListSchema,
CreateExceptionListItemSchema,
UpdateExceptionListItemSchema,
- Entry,
- EntryExists,
- EntryMatch,
- EntryMatchAny,
- EntryMatchWildcard,
- EntryNested,
- EntryList,
- EntriesArray,
- NamespaceType,
- Operator,
- OperatorEnum,
- OperatorTypeEnum,
- ExceptionListTypeEnum,
exceptionListItemSchema,
- exceptionListType,
- comment,
createExceptionListItemSchema,
listSchema,
- entry,
- entriesNested,
- nestedEntryItem,
- entriesMatch,
- entriesMatchAny,
- entriesMatchWildcard,
- entriesExists,
- entriesList,
- namespaceType,
- ExceptionListType,
- Type,
ENDPOINT_LIST_ID,
ENDPOINT_TRUSTED_APPS_LIST_ID,
EXCEPTION_LIST_URL,
@@ -52,8 +22,5 @@ export {
ENDPOINT_EVENT_FILTERS_LIST_ID,
ENDPOINT_EVENT_FILTERS_LIST_NAME,
ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
- osType,
- osTypeArray,
- OsTypeArray,
buildExceptionFilter,
} from '../../lists/common';
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx
index c1efb4d7c4565..9cb219e7a8d45 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx
@@ -15,10 +15,11 @@ import {
} from '@elastic/eui';
import { uniq } from 'lodash';
+import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete';
import { paramIsValid, getGenericComboBoxProps } from './helpers';
-import { OperatorTypeEnum } from '../../../lists_plugin_deps';
+
import { GetGenericComboBoxPropsReturn } from './types';
import * as i18n from './translations';
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx
index e77bf570adc63..dbfdaf9749b6d 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx
@@ -9,11 +9,12 @@ import React, { useState, useCallback, useMemo } from 'react';
import { EuiFormRow, EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui';
import { uniq } from 'lodash';
+import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete';
import { getGenericComboBoxProps, paramIsValid } from './helpers';
-import { OperatorTypeEnum } from '../../../lists_plugin_deps';
import { GetGenericComboBoxPropsReturn } from './types';
+
import * as i18n from './translations';
interface AutocompleteFieldMatchAnyProps {
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts
index b89f9525024c7..bd79bb0fcc8e8 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts
@@ -8,6 +8,8 @@
import dateMath from '@elastic/datemath';
import { EuiComboBoxOptionOption } from '@elastic/eui';
+import type { Type } from '@kbn/securitysolution-io-ts-list-types';
+import type { ListSchema } from '../../../lists_plugin_deps';
import { IFieldType } from '../../../../../../../src/plugins/data/common';
import {
@@ -19,7 +21,6 @@ import {
} from './operators';
import { GetGenericComboBoxPropsReturn, OperatorOption } from './types';
import * as i18n from './translations';
-import { ListSchema, Type } from '../../../lists_plugin_deps';
/**
* Returns the appropriate operators given a field type
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts
index 36e050c84f0b3..e0bdbf2603dc3 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts
@@ -15,7 +15,7 @@ import {
import { useKibana } from '../../../../common/lib/kibana';
import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub';
import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
-import { OperatorTypeEnum } from '../../../../lists_plugin_deps';
+import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
jest.mock('../../../../common/lib/kibana');
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts
index b8440205e7d32..0f369fa01d01e 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts
@@ -8,9 +8,9 @@
import { useEffect, useState, useRef } from 'react';
import { debounce } from 'lodash';
+import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common';
import { useKibana } from '../../../../common/lib/kibana';
-import { OperatorTypeEnum } from '../../../../lists_plugin_deps';
interface FuncArgs {
fieldSelected: IFieldType | undefined;
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts
index 93eab41264bf7..53e2ddf84b3d3 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts
@@ -6,8 +6,11 @@
*/
import { i18n } from '@kbn/i18n';
+import {
+ ListOperatorEnum as OperatorEnum,
+ ListOperatorTypeEnum as OperatorTypeEnum,
+} from '@kbn/securitysolution-io-ts-list-types';
import { OperatorOption } from './types';
-import { OperatorEnum, OperatorTypeEnum } from '../../../lists_plugin_deps';
export const isOperator: OperatorOption = {
message: i18n.translate('xpack.securitySolution.exceptions.isOperatorLabel', {
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts
index 903edc403ea25..1d8e3e9aee28e 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts
@@ -7,7 +7,10 @@
import { EuiComboBoxOptionOption } from '@elastic/eui';
-import { OperatorEnum, OperatorTypeEnum } from '../../../lists_plugin_deps';
+import type {
+ ListOperatorEnum as OperatorEnum,
+ ListOperatorTypeEnum as OperatorTypeEnum,
+} from '@kbn/securitysolution-io-ts-list-types';
export interface GetGenericComboBoxPropsReturn {
comboOptions: EuiComboBoxOptionOption[];
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx
index c627363fc29ef..c13a1b011ccbd 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx
@@ -17,7 +17,7 @@ import {
EuiCommentProps,
EuiText,
} from '@elastic/eui';
-import { Comment } from '../../../shared_imports';
+import type { Comment } from '@kbn/securitysolution-io-ts-list-types';
import * as i18n from './translations';
import { useCurrentUser } from '../../lib/kibana';
import { getFormattedComments } from './helpers';
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx
index 5ec8999d20518..5fb527a821bac 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx
@@ -49,7 +49,10 @@ jest.mock('../../../containers/source');
jest.mock('../../../../detections/containers/detection_engine/rules');
jest.mock('../use_add_exception');
jest.mock('../use_fetch_or_create_rule_exception_list');
-jest.mock('../../../../shared_imports');
+jest.mock('../../../../shared_imports', () => ({
+ ...jest.requireActual('../../../../shared_imports'),
+ useAsync: jest.fn(),
+}));
jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async');
describe('When the add exception modal is opened', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
index 120c4ad8efc1b..6efbbcf64406b 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
@@ -25,6 +25,7 @@ import {
EuiComboBox,
EuiComboBoxOptionOption,
} from '@elastic/eui';
+import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
import {
hasEqlSequenceQuery,
isEqlRule,
@@ -34,9 +35,9 @@ import { Status } from '../../../../../common/detection_engine/schemas/common/sc
import {
ExceptionListItemSchema,
CreateExceptionListItemSchema,
- ExceptionListType,
ExceptionBuilder,
} from '../../../../../public/shared_imports';
+
import * as i18nCommon from '../../../translations';
import * as i18n from './translations';
import * as sharedI18n from '../translations';
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
index 5fb52994fb0f5..6c68dcf934b71 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
@@ -22,6 +22,7 @@ import {
EuiCallOut,
} from '@elastic/eui';
+import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
import {
hasEqlSequenceQuery,
isEqlRule,
@@ -33,9 +34,9 @@ import { useRuleAsync } from '../../../../detections/containers/detection_engine
import {
ExceptionListItemSchema,
CreateExceptionListItemSchema,
- ExceptionListType,
ExceptionBuilder,
} from '../../../../../public/shared_imports';
+
import * as i18n from './translations';
import * as sharedI18n from '../translations';
import { useKibana } from '../../../lib/kibana';
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
index 907b30fcaa879..98c2b4db5676e 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
@@ -10,13 +10,8 @@ import { mount } from 'enzyme';
import moment from 'moment-timezone';
import {
- getOperatorType,
- getExceptionOperatorSelect,
getFormattedComments,
- filterExceptionItems,
- getNewExceptionItem,
formatOperatingSystems,
- getEntryValue,
formatExceptionItemForUpdate,
enrichNewExceptionItemsWithComments,
enrichExistingExceptionItemWithComments,
@@ -32,35 +27,19 @@ import {
retrieveAlertOsTypes,
filterIndexPatterns,
} from './helpers';
-import { AlertData, EmptyEntry } from './types';
+import { AlertData } from './types';
import {
- isOperator,
- isNotOperator,
- isOneOfOperator,
- isNotOneOfOperator,
- isInListOperator,
- isNotInListOperator,
- existsOperator,
- doesNotExistOperator,
-} from '../autocomplete/operators';
-import { OperatorTypeEnum, OperatorEnum, EntryNested } from '../../../shared_imports';
+ ListOperatorTypeEnum as OperatorTypeEnum,
+ EntriesArray,
+ OsTypeArray,
+} from '@kbn/securitysolution-io-ts-list-types';
+
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getEntryMatchMock } from '../../../../../lists/common/schemas/types/entry_match.mock';
-import { getEntryMatchAnyMock } from '../../../../../lists/common/schemas/types/entry_match_any.mock';
-import { getEntryExistsMock } from '../../../../../lists/common/schemas/types/entry_exists.mock';
-import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock';
import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comment.mock';
import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
-import {
- ENTRIES,
- ENTRIES_WITH_IDS,
- OLD_DATE_RELATIVE_TO_DATE_NOW,
-} from '../../../../../lists/common/constants.mock';
-import { EntriesArray, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types';
-import {
- CreateExceptionListItemSchema,
- ExceptionListItemSchema,
-} from '../../../../../lists/common/schemas';
+import { ENTRIES, OLD_DATE_RELATIVE_TO_DATE_NOW } from '../../../../../lists/common/constants.mock';
+import { ExceptionListItemSchema } from '../../../../../lists/common/schemas';
import { IFieldType, IIndexPattern } from 'src/plugins/data/common';
jest.mock('uuid', () => ({
@@ -162,128 +141,6 @@ describe('Exception helpers', () => {
});
});
- describe('#getOperatorType', () => {
- test('returns operator type "match" if entry.type is "match"', () => {
- const payload = getEntryMatchMock();
- const operatorType = getOperatorType(payload);
-
- expect(operatorType).toEqual(OperatorTypeEnum.MATCH);
- });
-
- test('returns operator type "match_any" if entry.type is "match_any"', () => {
- const payload = getEntryMatchAnyMock();
- const operatorType = getOperatorType(payload);
-
- expect(operatorType).toEqual(OperatorTypeEnum.MATCH_ANY);
- });
-
- test('returns operator type "list" if entry.type is "list"', () => {
- const payload = getEntryListMock();
- const operatorType = getOperatorType(payload);
-
- expect(operatorType).toEqual(OperatorTypeEnum.LIST);
- });
-
- test('returns operator type "exists" if entry.type is "exists"', () => {
- const payload = getEntryExistsMock();
- const operatorType = getOperatorType(payload);
-
- expect(operatorType).toEqual(OperatorTypeEnum.EXISTS);
- });
- });
-
- describe('#getExceptionOperatorSelect', () => {
- test('it returns "isOperator" when "operator" is "included" and operator type is "match"', () => {
- const payload = getEntryMatchMock();
- const result = getExceptionOperatorSelect(payload);
-
- expect(result).toEqual(isOperator);
- });
-
- test('it returns "isNotOperator" when "operator" is "excluded" and operator type is "match"', () => {
- const payload = getEntryMatchMock();
- payload.operator = 'excluded';
- const result = getExceptionOperatorSelect(payload);
-
- expect(result).toEqual(isNotOperator);
- });
-
- test('it returns "isOneOfOperator" when "operator" is "included" and operator type is "match_any"', () => {
- const payload = getEntryMatchAnyMock();
- const result = getExceptionOperatorSelect(payload);
-
- expect(result).toEqual(isOneOfOperator);
- });
-
- test('it returns "isNotOneOfOperator" when "operator" is "excluded" and operator type is "match_any"', () => {
- const payload = getEntryMatchAnyMock();
- payload.operator = 'excluded';
- const result = getExceptionOperatorSelect(payload);
-
- expect(result).toEqual(isNotOneOfOperator);
- });
-
- test('it returns "existsOperator" when "operator" is "included" and no operator type is provided', () => {
- const payload = getEntryExistsMock();
- const result = getExceptionOperatorSelect(payload);
-
- expect(result).toEqual(existsOperator);
- });
-
- test('it returns "doesNotExistsOperator" when "operator" is "excluded" and no operator type is provided', () => {
- const payload = getEntryExistsMock();
- payload.operator = 'excluded';
- const result = getExceptionOperatorSelect(payload);
-
- expect(result).toEqual(doesNotExistOperator);
- });
-
- test('it returns "isInList" when "operator" is "included" and operator type is "list"', () => {
- const payload = getEntryListMock();
- const result = getExceptionOperatorSelect(payload);
-
- expect(result).toEqual(isInListOperator);
- });
-
- test('it returns "isNotInList" when "operator" is "excluded" and operator type is "list"', () => {
- const payload = getEntryListMock();
- payload.operator = 'excluded';
- const result = getExceptionOperatorSelect(payload);
-
- expect(result).toEqual(isNotInListOperator);
- });
- });
-
- describe('#getEntryValue', () => {
- it('returns "match" entry value', () => {
- const payload = getEntryMatchMock();
- const result = getEntryValue(payload);
- const expected = 'some host name';
- expect(result).toEqual(expected);
- });
-
- it('returns "match any" entry values', () => {
- const payload = getEntryMatchAnyMock();
- const result = getEntryValue(payload);
- const expected = ['some host name'];
- expect(result).toEqual(expected);
- });
-
- it('returns "exists" entry value', () => {
- const payload = getEntryExistsMock();
- const result = getEntryValue(payload);
- const expected = undefined;
- expect(result).toEqual(expected);
- });
-
- it('returns "list" entry value', () => {
- const payload = getEntryListMock();
- const result = getEntryValue(payload);
- const expected = 'some-list-id';
- expect(result).toEqual(expected);
- });
- });
-
describe('#formatOperatingSystems', () => {
test('it returns null if no operating system tag specified', () => {
const result = formatOperatingSystems(['some os', 'some other os']);
@@ -324,178 +181,6 @@ describe('Exception helpers', () => {
});
});
- describe('#filterExceptionItems', () => {
- // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes
- // for context around the temporary `id`
- test('it correctly validates entries that include a temporary `id`', () => {
- const output: Array<
- ExceptionListItemSchema | CreateExceptionListItemSchema
- > = filterExceptionItems([
- { ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS },
- ]);
-
- expect(output).toEqual([{ ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }]);
- });
-
- test('it removes entry items with "value" of "undefined"', () => {
- const { entries, ...rest } = getExceptionListItemSchemaMock();
- const mockEmptyException: EmptyEntry = {
- id: '123',
- field: 'host.name',
- type: OperatorTypeEnum.MATCH,
- operator: OperatorEnum.INCLUDED,
- value: undefined,
- };
- const exceptions = filterExceptionItems([
- {
- ...rest,
- entries: [...entries, mockEmptyException],
- },
- ]);
-
- expect(exceptions).toEqual([getExceptionListItemSchemaMock()]);
- });
-
- test('it removes "match" entry items with "value" of empty string', () => {
- const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
- const mockEmptyException: EmptyEntry = {
- id: '123',
- field: 'host.name',
- type: OperatorTypeEnum.MATCH,
- operator: OperatorEnum.INCLUDED,
- value: '',
- };
- const output: Array<
- ExceptionListItemSchema | CreateExceptionListItemSchema
- > = filterExceptionItems([
- {
- ...rest,
- entries: [...entries, mockEmptyException],
- },
- ]);
-
- expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
- });
-
- test('it removes "match" entry items with "field" of empty string', () => {
- const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
- const mockEmptyException: EmptyEntry = {
- id: '123',
- field: '',
- type: OperatorTypeEnum.MATCH,
- operator: OperatorEnum.INCLUDED,
- value: 'some value',
- };
- const output: Array<
- ExceptionListItemSchema | CreateExceptionListItemSchema
- > = filterExceptionItems([
- {
- ...rest,
- entries: [...entries, mockEmptyException],
- },
- ]);
-
- expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
- });
-
- test('it removes "match_any" entry items with "field" of empty string', () => {
- const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
- const mockEmptyException: EmptyEntry = {
- id: '123',
- field: '',
- type: OperatorTypeEnum.MATCH_ANY,
- operator: OperatorEnum.INCLUDED,
- value: ['some value'],
- };
- const output: Array<
- ExceptionListItemSchema | CreateExceptionListItemSchema
- > = filterExceptionItems([
- {
- ...rest,
- entries: [...entries, mockEmptyException],
- },
- ]);
-
- expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
- });
-
- test('it removes "nested" entry items with "field" of empty string', () => {
- const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
- const mockEmptyException: EntryNested = {
- field: '',
- type: OperatorTypeEnum.NESTED,
- entries: [getEntryMatchMock()],
- };
- const output: Array<
- ExceptionListItemSchema | CreateExceptionListItemSchema
- > = filterExceptionItems([
- {
- ...rest,
- entries: [...entries, mockEmptyException],
- },
- ]);
-
- expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
- });
-
- test('it removes the "nested" entry entries with "value" of empty string', () => {
- const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
- const mockEmptyException: EntryNested = {
- field: 'host.name',
- type: OperatorTypeEnum.NESTED,
- entries: [getEntryMatchMock(), { ...getEntryMatchMock(), value: '' }],
- };
- const output: Array<
- ExceptionListItemSchema | CreateExceptionListItemSchema
- > = filterExceptionItems([
- {
- ...rest,
- entries: [...entries, mockEmptyException],
- },
- ]);
-
- expect(output).toEqual([
- {
- ...getExceptionListItemSchemaMock(),
- entries: [
- ...getExceptionListItemSchemaMock().entries,
- { ...mockEmptyException, entries: [getEntryMatchMock()] },
- ],
- },
- ]);
- });
-
- test('it removes the "nested" entry item if all its entries are invalid', () => {
- const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
- const mockEmptyException: EntryNested = {
- field: 'host.name',
- type: OperatorTypeEnum.NESTED,
- entries: [{ ...getEntryMatchMock(), value: '' }],
- };
- const output: Array<
- ExceptionListItemSchema | CreateExceptionListItemSchema
- > = filterExceptionItems([
- {
- ...rest,
- entries: [...entries, mockEmptyException],
- },
- ]);
-
- expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
- });
-
- test('it removes `temporaryId` from items', () => {
- const { meta, ...rest } = getNewExceptionItem({
- listId: '123',
- namespaceType: 'single',
- ruleName: 'rule name',
- });
- const exceptions = filterExceptionItems([{ ...rest, meta }]);
-
- expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]);
- });
- });
-
describe('#formatExceptionItemForUpdate', () => {
test('it should return correct update fields', () => {
const payload = getExceptionListItemSchemaMock();
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
index ce76114309e2e..437e93bb26fef 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
@@ -9,46 +9,36 @@ import React from 'react';
import { EuiText, EuiCommentProps, EuiAvatar } from '@elastic/eui';
import { capitalize } from 'lodash';
import moment from 'moment';
-import uuid from 'uuid';
-import * as i18n from './translations';
-import {
- AlertData,
- BuilderEntry,
- CreateExceptionListItemBuilderSchema,
- ExceptionsBuilderExceptionItem,
- Flattened,
-} from './types';
-import { EXCEPTION_OPERATORS, isOperator } from '../autocomplete/operators';
-import { OperatorOption } from '../autocomplete/types';
import {
+ comment,
+ osType,
CommentsArray,
Comment,
CreateComment,
Entry,
- ExceptionListItemSchema,
NamespaceType,
- OperatorTypeEnum,
- CreateExceptionListItemSchema,
- comment,
- entry,
- entriesNested,
- nestedEntryItem,
- createExceptionListItemSchema,
- exceptionListItemSchema,
- UpdateExceptionListItemSchema,
EntryNested,
OsTypeArray,
- EntriesArray,
- osType,
ExceptionListType,
+ ListOperatorTypeEnum as OperatorTypeEnum,
+} from '@kbn/securitysolution-io-ts-list-types';
+
+import * as i18n from './translations';
+import { AlertData, ExceptionsBuilderExceptionItem, Flattened } from './types';
+import {
+ ExceptionListItemSchema,
+ CreateExceptionListItemSchema,
+ UpdateExceptionListItemSchema,
+ getOperatorType,
+ getNewExceptionItem,
+ addIdToEntries,
} from '../../../shared_imports';
+
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
-import { validate } from '../../../../common/validate';
import { Ecs } from '../../../../common/ecs';
import { CodeSignature } from '../../../../common/ecs/file';
import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard';
-import { addIdToItem, removeIdFromItem } from '../../../../common';
import exceptionableLinuxFields from './exceptionable_linux_fields.json';
import exceptionableWindowsMacFields from './exceptionable_windows_mac_fields.json';
import exceptionableEndpointFields from './exceptionable_endpoint_fields.json';
@@ -84,75 +74,6 @@ export const filterIndexPatterns = (
}
};
-export const addIdToEntries = (entries: EntriesArray): EntriesArray => {
- return entries.map((singleEntry) => {
- if (singleEntry.type === 'nested') {
- return addIdToItem({
- ...singleEntry,
- entries: singleEntry.entries.map((nestedEntry) => addIdToItem(nestedEntry)),
- });
- } else {
- return addIdToItem(singleEntry);
- }
- });
-};
-
-/**
- * Returns the operator type, may not need this if using io-ts types
- *
- * @param item a single ExceptionItem entry
- */
-export const getOperatorType = (item: BuilderEntry): OperatorTypeEnum => {
- switch (item.type) {
- case 'match':
- return OperatorTypeEnum.MATCH;
- case 'match_any':
- return OperatorTypeEnum.MATCH_ANY;
- case 'list':
- return OperatorTypeEnum.LIST;
- default:
- return OperatorTypeEnum.EXISTS;
- }
-};
-
-/**
- * Determines operator selection (is/is not/is one of, etc.)
- * Default operator is "is"
- *
- * @param item a single ExceptionItem entry
- */
-export const getExceptionOperatorSelect = (item: BuilderEntry): OperatorOption => {
- if (item.type === 'nested') {
- return isOperator;
- } else {
- const operatorType = getOperatorType(item);
- const foundOperator = EXCEPTION_OPERATORS.find((operatorOption) => {
- return item.operator === operatorOption.operator && operatorType === operatorOption.type;
- });
-
- return foundOperator ?? isOperator;
- }
-};
-
-/**
- * Returns the fields corresponding value for an entry
- *
- * @param item a single ExceptionItem entry
- */
-export const getEntryValue = (item: BuilderEntry): string | string[] | undefined => {
- switch (item.type) {
- case OperatorTypeEnum.MATCH:
- case OperatorTypeEnum.MATCH_ANY:
- return item.value;
- case OperatorTypeEnum.EXISTS:
- return undefined;
- case OperatorTypeEnum.LIST:
- return item.list.id;
- default:
- return undefined;
- }
-};
-
/**
* Formats os value array to a displayable string
*/
@@ -189,91 +110,6 @@ export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[]
),
}));
-export const getNewExceptionItem = ({
- listId,
- namespaceType,
- ruleName,
-}: {
- listId: string;
- namespaceType: NamespaceType;
- ruleName: string;
-}): CreateExceptionListItemBuilderSchema => {
- return {
- comments: [],
- description: `${ruleName} - exception list item`,
- entries: addIdToEntries([
- {
- field: '',
- operator: 'included',
- type: 'match',
- value: '',
- },
- ]),
- item_id: undefined,
- list_id: listId,
- meta: {
- temporaryUuid: uuid.v4(),
- },
- name: `${ruleName} - exception list item`,
- namespace_type: namespaceType,
- tags: [],
- type: 'simple',
- };
-};
-
-export const filterExceptionItems = (
- exceptions: ExceptionsBuilderExceptionItem[]
-): Array => {
- return exceptions.reduce>(
- (acc, exception) => {
- const entries = exception.entries.reduce((nestedAcc, singleEntry) => {
- const strippedSingleEntry = removeIdFromItem(singleEntry);
-
- if (entriesNested.is(strippedSingleEntry)) {
- const nestedEntriesArray = strippedSingleEntry.entries.filter((singleNestedEntry) => {
- const noIdSingleNestedEntry = removeIdFromItem(singleNestedEntry);
- const [validatedNestedEntry] = validate(noIdSingleNestedEntry, nestedEntryItem);
- return validatedNestedEntry != null;
- });
- const noIdNestedEntries = nestedEntriesArray.map((singleNestedEntry) =>
- removeIdFromItem(singleNestedEntry)
- );
-
- const [validatedNestedEntry] = validate(
- { ...strippedSingleEntry, entries: noIdNestedEntries },
- entriesNested
- );
-
- if (validatedNestedEntry != null) {
- return [...nestedAcc, { ...singleEntry, entries: nestedEntriesArray }];
- }
- return nestedAcc;
- } else {
- const [validatedEntry] = validate(strippedSingleEntry, entry);
-
- if (validatedEntry != null) {
- return [...nestedAcc, singleEntry];
- }
- return nestedAcc;
- }
- }, []);
-
- const item = { ...exception, entries };
-
- if (exceptionListItemSchema.is(item)) {
- return [...acc, item];
- } else if (createExceptionListItemSchema.is(item)) {
- const { meta, ...rest } = item;
- const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined };
- return [...acc, itemSansMetaId];
- } else {
- return acc;
- }
- },
- []
- );
-};
-
export const formatExceptionItemForUpdate = (
exceptionItem: ExceptionListItemSchema
): UpdateExceptionListItemSchema => {
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts
index 92a3cb2cfac93..49cdd7103c48b 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts
@@ -6,23 +6,22 @@
*/
import { ReactNode } from 'react';
-import { Ecs } from '../../../../common/ecs';
-import { CodeSignature } from '../../../../common/ecs/file';
-import { IFieldType } from '../../../../../../../src/plugins/data/common';
-import { OperatorOption } from '../autocomplete/types';
-import {
+import type {
EntryNested,
Entry,
EntryMatch,
EntryMatchAny,
EntryMatchWildcard,
EntryExists,
- ExceptionListItemSchema,
- CreateExceptionListItemSchema,
NamespaceType,
- OperatorTypeEnum,
- OperatorEnum,
-} from '../../../lists_plugin_deps';
+ ListOperatorTypeEnum as OperatorTypeEnum,
+ ListOperatorEnum as OperatorEnum,
+} from '@kbn/securitysolution-io-ts-list-types';
+import { Ecs } from '../../../../common/ecs';
+import { CodeSignature } from '../../../../common/ecs/file';
+import { IFieldType } from '../../../../../../../src/plugins/data/common';
+import { OperatorOption } from '../autocomplete/types';
+import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../lists_plugin_deps';
export interface FormattedEntry {
fieldName: string;
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx
index 0f6dd19ea9b66..f609acf9c6c63 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx
@@ -27,6 +27,7 @@ import {
ReturnUseAddOrUpdateException,
AddOrUpdateExceptionItemsFunc,
} from './use_add_exception';
+import { UpdateDocumentByQueryResponse } from 'elasticsearch';
const mockKibanaHttpService = coreMock.createStart().http;
const mockKibanaServices = KibanaServices.get as jest.Mock;
@@ -36,11 +37,9 @@ const fetchMock = jest.fn();
mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } });
describe('useAddOrUpdateException', () => {
- let updateAlertStatus: jest.SpyInstance>;
- let addExceptionListItem: jest.SpyInstance>;
- let updateExceptionListItem: jest.SpyInstance<
- ReturnType
- >;
+ let updateAlertStatus: jest.SpyInstance>;
+ let addExceptionListItem: jest.SpyInstance>;
+ let updateExceptionListItem: jest.SpyInstance>;
let getQueryFilter: jest.SpyInstance>;
let buildAlertStatusFilter: jest.SpyInstance<
ReturnType
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx
index 877f545b69d65..17237f4f94c61 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx
@@ -12,7 +12,7 @@ import * as rulesApi from '../../../detections/containers/detection_engine/rules
import * as listsApi from '../../../../../lists/public/exceptions/api';
import { getExceptionListSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_schema.mock';
import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock';
-import { ExceptionListType } from '../../../lists_plugin_deps';
+import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
import { ListArray } from '../../../../common/detection_engine/schemas/types';
import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock';
import {
@@ -20,6 +20,7 @@ import {
UseFetchOrCreateRuleExceptionListProps,
ReturnUseFetchOrCreateRuleExceptionList,
} from './use_fetch_or_create_rule_exception_list';
+import { ExceptionListSchema } from '../../../shared_imports';
const mockKibanaHttpService = coreMock.createStart().http;
jest.mock('../../../detections/containers/detection_engine/rules/api');
@@ -31,7 +32,7 @@ describe('useFetchOrCreateRuleExceptionList', () => {
let addEndpointExceptionList: jest.SpyInstance<
ReturnType
>;
- let fetchExceptionListById: jest.SpyInstance>;
+ let fetchExceptionListById: jest.SpyInstance>;
let render: (
listType?: UseFetchOrCreateRuleExceptionListProps['exceptionListType']
) => RenderHookResult<
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx
index 8ded1b902f302..4f78b49ea266c 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx
@@ -11,8 +11,8 @@ import React from 'react';
import { ThemeProvider } from 'styled-components';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
+import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { ExceptionsViewerHeader } from './exceptions_viewer_header';
-import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps';
addDecorator((storyFn) => (
({ eui: euiLightVars, darkMode: false })}>{storyFn()}
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx
index b82a472befdcf..7dcd59069b53c 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx
@@ -9,7 +9,8 @@ import React from 'react';
import { mount } from 'enzyme';
import { ExceptionsViewerHeader } from './exceptions_viewer_header';
-import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps';
+
+import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
describe('ExceptionsViewerHeader', () => {
it('it renders all disabled if "isInitLoading" is true', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx
index eff4368ef6809..8fc28ad89156d 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx
@@ -18,9 +18,9 @@ import {
} from '@elastic/eui';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
+import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import * as i18n from '../translations';
import { Filter } from '../types';
-import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps';
interface ExceptionsViewerHeaderProps {
isInitLoading: boolean;
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx
index 29764625075d6..abd45cf2945cb 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx
@@ -7,8 +7,14 @@
import moment from 'moment';
-import { entriesNested, ExceptionListItemSchema } from '../../../../lists_plugin_deps';
-import { getEntryValue, getExceptionOperatorSelect, formatOperatingSystems } from '../helpers';
+import { entriesNested } from '@kbn/securitysolution-io-ts-list-types';
+import {
+ ExceptionListItemSchema,
+ getEntryValue,
+ getExceptionOperatorSelect,
+} from '../../../../lists_plugin_deps';
+
+import { formatOperatingSystems } from '../helpers';
import { FormattedEntry, BuilderEntry, DescriptionListItem } from '../types';
import * as i18n from '../translations';
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx
index 3fe6497105af1..971b3fda47191 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx
@@ -11,11 +11,9 @@ import { ThemeProvider } from 'styled-components';
import { ExceptionsViewer } from './';
import { useKibana } from '../../../../common/lib/kibana';
-import {
- ExceptionListTypeEnum,
- useExceptionListItems,
- useApi,
-} from '../../../../../public/lists_plugin_deps';
+import { useExceptionListItems, useApi } from '../../../../../public/lists_plugin_deps';
+
+import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock';
import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';
import { getMockTheme } from '../../../lib/kibana/kibana_react.mock';
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx
index 8c4569ed29b33..da7607f40ab72 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx
@@ -9,6 +9,7 @@ import React, { useCallback, useEffect, useReducer } from 'react';
import { EuiSpacer } from '@elastic/eui';
import uuid from 'uuid';
+import type { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import * as i18n from '../translations';
import { useStateToaster } from '../../toasters';
import { useKibana } from '../../../../common/lib/kibana';
@@ -20,11 +21,11 @@ import { allExceptionItemsReducer, State, ViewerModalName } from './reducer';
import {
useExceptionListItems,
ExceptionListIdentifiers,
- ExceptionListTypeEnum,
ExceptionListItemSchema,
UseExceptionListItemsSuccess,
useApi,
} from '../../../../../public/lists_plugin_deps';
+
import { ExceptionsViewerPagination } from './exceptions_pagination';
import { ExceptionsViewerUtility } from './exceptions_utility';
import { ExceptionsViewerItems } from './exceptions_viewer_items';
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts
index 46ac19f47503d..bf8e454e9971f 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
import {
FilterOptions,
ExceptionsPagination,
@@ -12,7 +13,6 @@ import {
Filter,
} from '../types';
import {
- ExceptionListType,
ExceptionListItemSchema,
ExceptionListIdentifiers,
Pagination,
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx
index 69e41a2c3d0a2..3152c08fab323 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx
@@ -19,6 +19,7 @@ import styled from 'styled-components';
import { getOr } from 'lodash/fp';
import { indexOf } from 'lodash';
+import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { TimelineId } from '../../../../../common/types/timeline';
@@ -44,7 +45,6 @@ import {
} from '../../../../common/components/toasters';
import { inputsModel } from '../../../../common/store';
import { useUserData } from '../../user_info';
-import { ExceptionListType } from '../../../../../common/shared_imports';
import { AlertData, EcsHit } from '../../../../common/components/exceptions/types';
import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query';
import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index';
diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx
index 94cb22592f4ed..ea903882c326d 100644
--- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx
@@ -18,7 +18,9 @@ import {
EuiSelectOption,
} from '@elastic/eui';
-import { useImportList, ListSchema, Type } from '../../../shared_imports';
+import type { Type } from '@kbn/securitysolution-io-ts-list-types';
+import { useImportList, ListSchema } from '../../../shared_imports';
+
import * as i18n from './translations';
import { useKibana } from '../../../common/lib/kibana';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx
index d11ceee7f5978..64cb936f160f1 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx
@@ -11,8 +11,8 @@ import React from 'react';
import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui';
import { History } from 'history';
+import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types';
import { Spacer } from '../../../../../../common/components/page';
-import { NamespaceType } from '../../../../../../../../lists/common';
import { FormatUrl } from '../../../../../../common/components/link_to';
import { LinkAnchor } from '../../../../../../common/components/links';
import * as i18n from './translations';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
index 146b7e8470718..50cf1b1830fec 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
@@ -15,9 +15,9 @@ import {
} from '@elastic/eui';
import { History } from 'history';
+import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types';
import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts';
import { AutoDownload } from '../../../../../../common/components/auto_download/auto_download';
-import { NamespaceType } from '../../../../../../../../lists/common';
import { useKibana } from '../../../../../../common/lib/kibana';
import { ExceptionListFilter, useApi, useExceptionLists } from '../../../../../../shared_imports';
import { FormatUrl } from '../../../../../../common/components/link_to';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
index 64dfac5787f23..29b63721513d4 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
@@ -9,11 +9,12 @@ import { has, isEmpty } from 'lodash/fp';
import moment from 'moment';
import deepmerge from 'deepmerge';
+import type { ExceptionListType, NamespaceType } from '@kbn/securitysolution-io-ts-list-types';
import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/constants';
import { assertUnreachable } from '../../../../../../common/utility_types';
import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions';
import { List } from '../../../../../../common/detection_engine/schemas/types';
-import { ENDPOINT_LIST_ID, ExceptionListType, NamespaceType } from '../../../../../shared_imports';
+import { ENDPOINT_LIST_ID } from '../../../../../shared_imports';
import { Rule } from '../../../../containers/detection_engine/rules';
import {
Threats,
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
index 0fab428ef6d1b..9660132147a57 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
@@ -28,6 +28,7 @@ import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
+import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import {
useDeepEqualSelector,
useShallowEqualSelector,
@@ -83,7 +84,8 @@ import { ExceptionsViewer } from '../../../../../common/components/exceptions/vi
import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants';
import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen';
import { Display } from '../../../../../hosts/pages/display';
-import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports';
+import type { ExceptionListIdentifiers } from '../../../../../shared_imports';
+
import {
focusUtilityBarAction,
onTimelineTabKeyPressed,
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts
index 5d600f471994b..e1fa1107fcb01 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts
@@ -5,9 +5,8 @@
* 2.0.
*/
+import { ExceptionListType, ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import {
- ExceptionListType,
- ExceptionListTypeEnum,
EXCEPTION_LIST_URL,
EXCEPTION_LIST_ITEM_URL,
ENDPOINT_EVENT_FILTERS_LIST_ID,
diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts
index e77c4a0eec486..76ec761d41703 100644
--- a/x-pack/plugins/security_solution/public/shared_imports.ts
+++ b/x-pack/plugins/security_solution/public/shared_imports.ts
@@ -33,23 +33,23 @@ export { ERROR_CODE } from '../../../../src/plugins/es_ui_shared/static/forms/he
export {
exportList,
- useIsMounted,
useCursor,
useApi,
useAsync,
useExceptionListItems,
useExceptionLists,
- usePersistExceptionItem,
- usePersistExceptionList,
useFindLists,
useDeleteList,
useImportList,
useCreateListIndex,
useReadListIndex,
useReadListPrivileges,
- addExceptionListItem,
- updateExceptionListItem,
fetchExceptionListById,
+ addIdToEntries,
+ getOperatorType,
+ getNewExceptionItem,
+ getEntryValue,
+ getExceptionOperatorSelect,
addExceptionList,
ExceptionListFilter,
ExceptionListIdentifiers,
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts
index 786a74e91b51a..e4704523a16c3 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts
@@ -7,17 +7,19 @@
import uuid from 'uuid';
-import { OsType } from '../../../../../lists/common/schemas';
-import {
+import type {
EntriesArray,
EntryMatch,
EntryMatchWildcard,
EntryNested,
- ExceptionListItemSchema,
NestedEntriesArray,
-} from '../../../../../lists/common';
+} from '@kbn/securitysolution-io-ts-list-types';
+
+import type { ExceptionListItemSchema } from '../../../../../lists/common';
+
+import type { OsType } from '../../../../../lists/common/schemas';
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants';
-import {
+import type {
CreateExceptionListItemOptions,
UpdateExceptionListItemOptions,
} from '../../../../../lists/server';
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts
index 3fa5d1178b3ec..578c1aba64558 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts
@@ -11,7 +11,7 @@ import { mockLogger, sampleDocWithSortId } from '../__mocks__/es_results';
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { listMock } from '../../../../../../lists/server/mocks';
import { getSearchListItemResponseMock } from '../../../../../../lists/common/schemas/response/search_list_item_schema.mock';
-import { EntryList } from '../../../../../../lists/common';
+import { EntryList } from '@kbn/securitysolution-io-ts-list-types';
import { buildRuleMessageMock as buildRuleMessage } from '../rule_messages.mock';
describe('filterEventsAgainstList', () => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts
index b2002dbb5a7e2..40322029c1d98 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EntryList, entriesList } from '../../../../../../lists/common';
+import { EntryList, entriesList } from '@kbn/securitysolution-io-ts-list-types';
import { createSetToFilterAgainst } from './create_set_to_filter_against';
import { CreateFieldAndSetTuplesOptions, FieldSet } from './types';
From ea8c92b353a094d64fed85f917e4c1dbc64a2774 Mon Sep 17 00:00:00 2001
From: Byron Hulcher
Date: Sat, 15 May 2021 01:10:53 -0400
Subject: [PATCH 08/11] [App Search] Allow user to manage source engines
through Kibana UX (#98866)
* New bulk create route for meta engine source engines
* New delete route for meta engine source engines
* Add removeSourceEngine and onSourceEngineRemove to SourceEnginesLogicActions
* New SourceEnginesTable component
* Use new SourceEnginesTable component in SourceEngines view
* Added closeAddSourceEnginesModal and openAddSourceEnginesModal to SourceEnginesLogic
* New AddSourceEnginesModal component
* New AddSourceEnginesButton component
* Add AddSourceEnginesButton and AddSourceEnginesModal to SourceEngines view
* Allow user to select source engines to add
* Add addSourceEngines and onSourceEnginesAdd to SourceEnginesLogic
* Submit new source engines when user saves from inside AddSourceEnginesModal
* Fix failing tests
* fix i18n
* Fix imports
* Use body instead of query params for source engines bulk create endpoint
* Tests for SouceEnginesLogic actions setIndexedEngines and fetchIndexedEngines
* Re-enabling two skipped tests
* Feedback: move source engine APIs to own file
- We generally organize routes/logic etc. by view, and since this is its own view, it can get its own file
* Misc UI polish
Table:
- Add EuiPageContent bordered panel (matches Curations & API logs which is a table in a panel)
- Remove bolding on engine name (matches rest of Kibana UI)
- Remove responsive false (we do want responsive tables in Kibana)
Modal:
- Remove EuiOverlayMask - per recent EUI changes, this now comes baked in with EuiModal
- Change description text to subdued to match other modals (e.g. Curations queries) in Kibana
* Misc i18n/copy tweaks
Modal:
- Add combobox placeholder text
- i18n cancel/save buttons
- inline i18n and change title casing to sentence casing
* Table refactors
- DRY out table columns shared with the main engines tables (title & formatting change slightly from the standalone UI, but this is fine / we should prefer Kibana standardization moving forward)
- Actions column changes
- Give it a name - axe will throw issues for table column missing headings
- Do not make actions a conditional empty array - we should opt to remove the column totally if there is no content present, otherwise screen readers will read out blank cells unnecessarily
- Switch to icons w/ description tooltips to match the other Kibana tables
- Remove unnecessary sorting props (we don't have sorting enabled on any columns)
Tests
- Add describe block for organization
- Add missing coverage for window confirm branch and canManageMetaEngineSourceEngines branch
* Modal test fixes
- Remove unnecessary type casting
- Remove commented out line
- Fix missing onChange function coverage
* Modal: move unmemoized array iterations to Kea selectors
- more performant: kea selectors are memoized
- cleaner/less logic in views
- easier to write unit tests for
+ rename setSelectedEngineNamesToAdd to onAddEnginesSelection
+ remove unused selectors test code
* Modal: Add isLoading UX to submit button + value renames
- isLoading prevents double clicks/dupe events, and also provides a responsive UX hint that something is happening
- Var renames: there's only one modal on the page, being extra specific with the name isn't really necessary. If we ever add more than one to this view it would probably make sense to split up the logic files or do something else. Verbose modal names/states shouldn't necessarily be the answer
* Source Engines view test fixes
- Remove unused mock values/actions
- Move constants to within main describe
- Remove unhappy vs happy path describes - there aren't enough of either scenario to warrant the distinction
- add page actions describe block and fix skipped/mounted test by shallow diving into EuiPageHeader
* [Misc] Single components/index.ts export
For easier group importing
* Move all copy consts/strings to their own i18n constants file
* Refactor recursive fetchEngines fn to shared util
+ update MetaEnginesTableLogic to use new helper/DRY out code
+ write unit tests for just that helper
+ simplify other previous logic checks to just check that the fn was called + add mock
* Tests cleanup
- Move consts into top of describe blocks to match rest of codebase
- Remove logic comments for files that are only sourcing 1 logic file
- Modal:
- shallow is fairly cheap and it's easier / more consistent w/ other tests to start a new wrapper every test
- Logic:
- Remove unnecessarily EnginesLogic mocks
- Remove mount() in beforeEach - it doesn't save us that many extra lines / better to be more consistent when starting tests that mount with values vs not
- mock clearing in beforeEach to match rest of codebase
- describe blocks: split up actions vs listeners, move selectors between the two
- actions: fix tests that are in a describe() but not an it() (incorrect syntax)
- Reducer/value checks: check against entire values obj to check for regressions or untested reducers & be consistent rest of codebase
- listeners - DRY out beforeEach of success vs error paths, combine some tests that are a bit repetitive vs just having multiple assertions
- Logic comments:
- Remove unnecessary comments (if we're not setting a response, it seems clear we're not using it)
- Add extra business logic context explanation as to why we call re-initialize the engine
Co-authored-by: Constance Chen
---
.../app_search/__mocks__/index.ts | 1 +
.../recursively_fetch_engines.mock.ts | 21 +
.../tables/meta_engines_table_logic.test.ts | 101 +----
.../tables/meta_engines_table_logic.ts | 41 +-
.../add_source_engines_button.test.tsx | 35 ++
.../components/add_source_engines_button.tsx | 25 ++
.../add_source_engines_modal.test.tsx | 103 +++++
.../components/add_source_engines_modal.tsx | 68 +++
.../source_engines/components/index.ts | 10 +
.../components/source_engines_table.test.tsx | 83 ++++
.../components/source_engines_table.tsx | 75 ++++
.../components/source_engines/i18n.ts | 67 +++
.../source_engines/source_engines.test.tsx | 80 +++-
.../source_engines/source_engines.tsx | 32 +-
.../source_engines_logic.test.ts | 423 ++++++++++++++----
.../source_engines/source_engines_logic.ts | 164 +++++--
.../recursively_fetch_engines/index.test.ts | 108 +++++
.../utils/recursively_fetch_engines/index.ts | 54 +++
.../server/routes/app_search/engines.test.ts | 43 --
.../server/routes/app_search/engines.ts | 17 -
.../server/routes/app_search/index.ts | 2 +
.../routes/app_search/source_engines.test.ts | 151 +++++++
.../routes/app_search/source_engines.ts | 65 +++
23 files changed, 1430 insertions(+), 339 deletions(-)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts
create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts
create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts
index 271a09849cba7..b444c1cc94383 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts
@@ -6,3 +6,4 @@
*/
export { mockEngineValues, mockEngineActions } from './engine_logic.mock';
+export { mockRecursivelyFetchEngines, mockSourceEngines } from './recursively_fetch_engines.mock';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts
new file mode 100644
index 0000000000000..dd4c86a2a6360
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EngineDetails } from '../components/engine/types';
+
+export const mockSourceEngines = [
+ { name: 'source-engine-1' },
+ { name: 'source-engine-2' },
+] as EngineDetails[];
+
+export const mockRecursivelyFetchEngines = jest.fn(({ onComplete }) =>
+ onComplete(mockSourceEngines)
+);
+
+jest.mock('../utils/recursively_fetch_engines', () => ({
+ recursivelyFetchEngines: mockRecursivelyFetchEngines,
+}));
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts
index b90207331ffd6..de1902c7cf748 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts
@@ -5,15 +5,16 @@
* 2.0.
*/
-import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__';
-
-import { nextTick } from '@kbn/test/jest';
+import { LogicMounter } from '../../../../../__mocks__';
+import { mockRecursivelyFetchEngines } from '../../../../__mocks__/recursively_fetch_engines.mock';
import { EngineDetails } from '../../../engine/types';
import { MetaEnginesTableLogic } from './meta_engines_table_logic';
describe('MetaEnginesTableLogic', () => {
+ const { mount } = new LogicMounter(MetaEnginesTableLogic);
+
const DEFAULT_VALUES = {
expandedRows: {},
sourceEngines: {},
@@ -44,15 +45,11 @@ describe('MetaEnginesTableLogic', () => {
metaEngines: [...SOURCE_ENGINES, ...META_ENGINES] as EngineDetails[],
};
- const { http } = mockHttpValues;
- const { mount } = new LogicMounter(MetaEnginesTableLogic);
- const { flashAPIErrors } = mockFlashMessageHelpers;
-
beforeEach(() => {
jest.clearAllMocks();
});
- it('has expected default values', async () => {
+ it('has expected default values', () => {
mount({}, DEFAULT_PROPS);
expect(MetaEnginesTableLogic.values).toEqual(DEFAULT_VALUES);
});
@@ -122,16 +119,6 @@ describe('MetaEnginesTableLogic', () => {
});
it('calls fetchSourceEngines when it needs to fetch data for the itemId', () => {
- http.get.mockReturnValueOnce(
- Promise.resolve({
- meta: {
- page: {
- total_pages: 1,
- },
- },
- results: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }],
- })
- );
mount();
jest.spyOn(MetaEnginesTableLogic.actions, 'fetchSourceEngines');
@@ -142,88 +129,22 @@ describe('MetaEnginesTableLogic', () => {
});
describe('fetchSourceEngines', () => {
- it('calls addSourceEngines and displayRow when it has retrieved all pages', async () => {
+ it('calls addSourceEngines and displayRow when it has retrieved all pages', () => {
mount();
- http.get.mockReturnValueOnce(
- Promise.resolve({
- meta: {
- page: {
- total_pages: 1,
- },
- },
- results: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }],
- })
- );
jest.spyOn(MetaEnginesTableLogic.actions, 'displayRow');
jest.spyOn(MetaEnginesTableLogic.actions, 'addSourceEngines');
MetaEnginesTableLogic.actions.fetchSourceEngines('test-engine-1');
- await nextTick();
-
- expect(http.get).toHaveBeenCalledWith(
- '/api/app_search/engines/test-engine-1/source_engines',
- {
- query: {
- 'page[current]': 1,
- 'page[size]': 25,
- },
- }
- );
- expect(MetaEnginesTableLogic.actions.addSourceEngines).toHaveBeenCalledWith({
- 'test-engine-1': [{ name: 'source-engine-1' }, { name: 'source-engine-2' }],
- });
- expect(MetaEnginesTableLogic.actions.displayRow).toHaveBeenCalledWith('test-engine-1');
- });
-
- it('display a flash message on error', async () => {
- http.get.mockReturnValueOnce(Promise.reject());
- mount();
- MetaEnginesTableLogic.actions.fetchSourceEngines('test-engine-1');
- await nextTick();
-
- expect(flashAPIErrors).toHaveBeenCalledTimes(1);
- });
-
- it('recursively fetches a number of pages', async () => {
- mount();
- jest.spyOn(MetaEnginesTableLogic.actions, 'addSourceEngines');
-
- // First page
- http.get.mockReturnValueOnce(
- Promise.resolve({
- meta: {
- page: {
- total_pages: 2,
- },
- },
- results: [{ name: 'source-engine-1' }],
- })
- );
-
- // Second and final page
- http.get.mockReturnValueOnce(
- Promise.resolve({
- meta: {
- page: {
- total_pages: 2,
- },
- },
- results: [{ name: 'source-engine-2' }],
+ expect(mockRecursivelyFetchEngines).toHaveBeenCalledWith(
+ expect.objectContaining({
+ endpoint: '/api/app_search/engines/test-engine-1/source_engines',
})
);
-
- MetaEnginesTableLogic.actions.fetchSourceEngines('test-engine-1');
- await nextTick();
-
expect(MetaEnginesTableLogic.actions.addSourceEngines).toHaveBeenCalledWith({
- 'test-engine-1': [
- // First page
- { name: 'source-engine-1' },
- // Second and final page
- { name: 'source-engine-2' },
- ],
+ 'test-engine-1': [{ name: 'source-engine-1' }, { name: 'source-engine-2' }],
});
+ expect(MetaEnginesTableLogic.actions.displayRow).toHaveBeenCalledWith('test-engine-1');
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts
index 3a4c7d51c50a9..af4d0119a94af 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts
@@ -7,10 +7,8 @@
import { kea, MakeLogicType } from 'kea';
-import { flashAPIErrors } from '../../../../../shared/flash_messages';
-import { HttpLogic } from '../../../../../shared/http';
+import { recursivelyFetchEngines } from '../../../../utils/recursively_fetch_engines';
import { EngineDetails } from '../../../engine/types';
-import { EnginesAPIResponse } from '../../types';
interface MetaEnginesTableValues {
expandedRows: { [id: string]: boolean };
@@ -85,36 +83,13 @@ export const MetaEnginesTableLogic = kea<
}
},
fetchSourceEngines: ({ engineName }) => {
- const { http } = HttpLogic.values;
-
- let enginesAccumulator: EngineDetails[] = [];
-
- const recursiveFetchSourceEngines = async (page = 1) => {
- try {
- const { meta, results }: EnginesAPIResponse = await http.get(
- `/api/app_search/engines/${engineName}/source_engines`,
- {
- query: {
- 'page[current]': page,
- 'page[size]': 25,
- },
- }
- );
-
- enginesAccumulator = [...enginesAccumulator, ...results];
-
- if (page >= meta.page.total_pages) {
- actions.addSourceEngines({ [engineName]: enginesAccumulator });
- actions.displayRow(engineName);
- } else {
- recursiveFetchSourceEngines(page + 1);
- }
- } catch (e) {
- flashAPIErrors(e);
- }
- };
-
- recursiveFetchSourceEngines();
+ recursivelyFetchEngines({
+ endpoint: `/api/app_search/engines/${engineName}/source_engines`,
+ onComplete: (sourceEngines) => {
+ actions.addSourceEngines({ [engineName]: sourceEngines });
+ actions.displayRow(engineName);
+ },
+ });
},
}),
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx
new file mode 100644
index 0000000000000..43a4682849c78
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 { setMockActions } from '../../../../__mocks__';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiButton } from '@elastic/eui';
+
+import { AddSourceEnginesButton } from './add_source_engines_button';
+
+describe('AddSourceEnginesButton', () => {
+ const MOCK_ACTIONS = {
+ openModal: jest.fn(),
+ };
+
+ it('opens the modal on click', () => {
+ setMockActions(MOCK_ACTIONS);
+
+ const wrapper = shallow();
+ const button = wrapper.find(EuiButton);
+
+ expect(button).toHaveLength(1);
+
+ button.simulate('click');
+
+ expect(MOCK_ACTIONS.openModal).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx
new file mode 100644
index 0000000000000..004217d88987b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx
@@ -0,0 +1,25 @@
+/*
+ * 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 from 'react';
+
+import { useActions } from 'kea';
+
+import { EuiButton } from '@elastic/eui';
+
+import { ADD_SOURCE_ENGINES_BUTTON_LABEL } from '../i18n';
+import { SourceEnginesLogic } from '../source_engines_logic';
+
+export const AddSourceEnginesButton: React.FC = () => {
+ const { openModal } = useActions(SourceEnginesLogic);
+
+ return (
+
+ {ADD_SOURCE_ENGINES_BUTTON_LABEL}
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx
new file mode 100644
index 0000000000000..19c2f72ed6f52
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx
@@ -0,0 +1,103 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { setMockActions, setMockValues } from '../../../../__mocks__';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiButton, EuiButtonEmpty, EuiComboBox, EuiModal } from '@elastic/eui';
+
+import { AddSourceEnginesModal } from './add_source_engines_modal';
+
+describe('AddSourceEnginesModal', () => {
+ const MOCK_VALUES = {
+ selectableEngineNames: ['source-engine-1', 'source-engine-2', 'source-engine-3'],
+ selectedEngineNamesToAdd: ['source-engine-2'],
+ modalLoading: false,
+ };
+
+ const MOCK_ACTIONS = {
+ addSourceEngines: jest.fn(),
+ closeModal: jest.fn(),
+ onAddEnginesSelection: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockValues(MOCK_VALUES);
+ setMockActions(MOCK_ACTIONS);
+ });
+
+ it('calls closeAddSourceEnginesModal when the modal is closed', () => {
+ const wrapper = shallow();
+ wrapper.find(EuiModal).simulate('close');
+
+ expect(MOCK_ACTIONS.closeModal).toHaveBeenCalled();
+ });
+
+ describe('combo box', () => {
+ it('has the proper options and selected options', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiComboBox).prop('options')).toEqual([
+ { label: 'source-engine-1' },
+ { label: 'source-engine-2' },
+ { label: 'source-engine-3' },
+ ]);
+ expect(wrapper.find(EuiComboBox).prop('selectedOptions')).toEqual([
+ { label: 'source-engine-2' },
+ ]);
+ });
+
+ it('calls setSelectedEngineNamesToAdd when changed', () => {
+ const wrapper = shallow();
+ wrapper.find(EuiComboBox).simulate('change', [{ label: 'source-engine-3' }]);
+
+ expect(MOCK_ACTIONS.onAddEnginesSelection).toHaveBeenCalledWith(['source-engine-3']);
+ });
+ });
+
+ describe('cancel button', () => {
+ it('calls closeModal when clicked', () => {
+ const wrapper = shallow();
+ wrapper.find(EuiButtonEmpty).simulate('click');
+
+ expect(MOCK_ACTIONS.closeModal).toHaveBeenCalled();
+ });
+ });
+
+ describe('save button', () => {
+ it('is disabled when user has selected no engines', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ selectedEngineNamesToAdd: [],
+ });
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiButton).prop('disabled')).toEqual(true);
+ });
+
+ it('passes modalLoading state', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ modalLoading: true,
+ });
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiButton).prop('isLoading')).toEqual(true);
+ });
+
+ it('calls addSourceEngines when clicked', () => {
+ const wrapper = shallow();
+ wrapper.find(EuiButton).simulate('click');
+
+ expect(MOCK_ACTIONS.addSourceEngines).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx
new file mode 100644
index 0000000000000..24e27e03818ad
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 from 'react';
+
+import { useActions, useValues } from 'kea';
+
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiComboBox,
+ EuiModalFooter,
+ EuiModal,
+ EuiModalBody,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+
+import { CANCEL_BUTTON_LABEL, SAVE_BUTTON_LABEL } from '../../../../shared/constants';
+
+import {
+ ADD_SOURCE_ENGINES_MODAL_TITLE,
+ ADD_SOURCE_ENGINES_MODAL_DESCRIPTION,
+ ADD_SOURCE_ENGINES_PLACEHOLDER,
+} from '../i18n';
+import { SourceEnginesLogic } from '../source_engines_logic';
+
+export const AddSourceEnginesModal: React.FC = () => {
+ const { addSourceEngines, closeModal, onAddEnginesSelection } = useActions(SourceEnginesLogic);
+ const { selectableEngineNames, selectedEngineNamesToAdd, modalLoading } = useValues(
+ SourceEnginesLogic
+ );
+
+ return (
+
+
+ {ADD_SOURCE_ENGINES_MODAL_TITLE}
+
+
+ {ADD_SOURCE_ENGINES_MODAL_DESCRIPTION}
+
+ ({ label: engineName }))}
+ selectedOptions={selectedEngineNamesToAdd.map((engineName) => ({ label: engineName }))}
+ onChange={(options) => onAddEnginesSelection(options.map((option) => option.label))}
+ placeholder={ADD_SOURCE_ENGINES_PLACEHOLDER}
+ />
+
+
+ {CANCEL_BUTTON_LABEL}
+ addSourceEngines(selectedEngineNamesToAdd)}
+ fill
+ >
+ {SAVE_BUTTON_LABEL}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts
new file mode 100644
index 0000000000000..edec07a70a0bf
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts
@@ -0,0 +1,10 @@
+/*
+ * 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 { AddSourceEnginesButton } from './add_source_engines_button';
+export { AddSourceEnginesModal } from './add_source_engines_modal';
+export { SourceEnginesTable } from './source_engines_table';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx
new file mode 100644
index 0000000000000..895c7ab35e86a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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 { mountWithIntl, setMockActions, setMockValues } from '../../../../__mocks__';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiInMemoryTable, EuiButtonIcon } from '@elastic/eui';
+
+import { SourceEnginesTable } from './source_engines_table';
+
+describe('SourceEnginesTable', () => {
+ const MOCK_VALUES = {
+ // AppLogic
+ myRole: {
+ canManageMetaEngineSourceEngines: true,
+ },
+ // SourceEnginesLogic
+ sourceEngines: [{ name: 'source-engine-1', document_count: 15, field_count: 26 }],
+ };
+
+ const MOCK_ACTIONS = {
+ removeSourceEngine: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockActions(MOCK_ACTIONS);
+ setMockValues(MOCK_VALUES);
+ });
+
+ it('renders', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1);
+ });
+
+ it('contains relevant informatiom from source engines', () => {
+ const wrapper = mountWithIntl();
+
+ expect(wrapper.find(EuiInMemoryTable).text()).toContain('source-engine-1');
+ expect(wrapper.find(EuiInMemoryTable).text()).toContain('15');
+ expect(wrapper.find(EuiInMemoryTable).text()).toContain('26');
+ });
+
+ describe('actions column', () => {
+ it('clicking a remove engine link calls a confirm dialogue before remove the engine', () => {
+ const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValueOnce(true);
+ const wrapper = mountWithIntl();
+
+ wrapper.find(EuiButtonIcon).simulate('click');
+
+ expect(confirmSpy).toHaveBeenCalled();
+ expect(MOCK_ACTIONS.removeSourceEngine).toHaveBeenCalled();
+ });
+
+ it('does not remove an engine if the user cancels the confirmation dialog', () => {
+ const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValueOnce(false);
+ const wrapper = mountWithIntl();
+
+ wrapper.find(EuiButtonIcon).simulate('click');
+
+ expect(confirmSpy).toHaveBeenCalled();
+ expect(MOCK_ACTIONS.removeSourceEngine).not.toHaveBeenCalled();
+ });
+
+ it('does not render the actions column if the user does not have permission to manage the engine', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ myRole: { canManageMetaEngineSourceEngines: false },
+ });
+ const wrapper = mountWithIntl();
+
+ expect(wrapper.find(EuiButtonIcon)).toHaveLength(0);
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx
new file mode 100644
index 0000000000000..f8c3e3ca00c95
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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 from 'react';
+
+import { useActions, useValues } from 'kea';
+
+import { EuiBasicTableColumn, EuiInMemoryTable } from '@elastic/eui';
+
+import { EuiLinkTo } from '../../../../shared/react_router_helpers';
+import { AppLogic } from '../../../app_logic';
+import { ENGINE_PATH } from '../../../routes';
+import { generateEncodedPath } from '../../../utils/encode_path_params';
+import { EngineDetails } from '../../engine/types';
+import {
+ NAME_COLUMN,
+ DOCUMENT_COUNT_COLUMN,
+ FIELD_COUNT_COLUMN,
+ ACTIONS_COLUMN,
+} from '../../engines/components/tables/shared_columns';
+
+import { REMOVE_SOURCE_ENGINE_BUTTON_LABEL, REMOVE_SOURCE_ENGINE_CONFIRM_DIALOGUE } from '../i18n';
+import { SourceEnginesLogic } from '../source_engines_logic';
+
+export const SourceEnginesTable: React.FC = () => {
+ const {
+ myRole: { canManageMetaEngineSourceEngines },
+ } = useValues(AppLogic);
+
+ const { removeSourceEngine } = useActions(SourceEnginesLogic);
+ const { sourceEngines } = useValues(SourceEnginesLogic);
+
+ const columns: Array> = [
+ {
+ ...NAME_COLUMN,
+ render: (engineName: string) => (
+ {engineName}
+ ),
+ },
+ DOCUMENT_COUNT_COLUMN,
+ FIELD_COUNT_COLUMN,
+ ];
+ if (canManageMetaEngineSourceEngines) {
+ columns.push({
+ name: ACTIONS_COLUMN.name,
+ actions: [
+ {
+ name: REMOVE_SOURCE_ENGINE_BUTTON_LABEL,
+ description: REMOVE_SOURCE_ENGINE_BUTTON_LABEL,
+ type: 'icon',
+ icon: 'trash',
+ color: 'danger',
+ onClick: (engine: EngineDetails) => {
+ if (confirm(REMOVE_SOURCE_ENGINE_CONFIRM_DIALOGUE(engine.name))) {
+ removeSourceEngine(engine.name);
+ }
+ },
+ },
+ ],
+ });
+ }
+
+ return (
+ 10}
+ search={{ box: { incremental: true } }}
+ />
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts
new file mode 100644
index 0000000000000..4e3f4f81d5a9f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const SOURCE_ENGINES_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.souceEngines.title',
+ { defaultMessage: 'Manage engines' }
+);
+
+export const ADD_SOURCE_ENGINES_BUTTON_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesButtonLabel',
+ { defaultMessage: 'Add engines' }
+);
+
+export const ADD_SOURCE_ENGINES_MODAL_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesModal.title',
+ { defaultMessage: 'Add engines' }
+);
+
+export const ADD_SOURCE_ENGINES_MODAL_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesModal.description',
+ { defaultMessage: 'Add additional engines to this meta engine.' }
+);
+
+export const ADD_SOURCE_ENGINES_PLACEHOLDER = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesPlaceholder',
+ { defaultMessage: 'Select engine(s)' }
+);
+
+export const ADD_SOURCE_ENGINES_SUCCESS_MESSAGE = (sourceEngineNames: string[]) =>
+ i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesSuccessMessage',
+ {
+ defaultMessage:
+ '{sourceEnginesCount, plural, one {# engine has} other {# engines have}} been added to this meta engine.',
+ values: { sourceEnginesCount: sourceEngineNames.length },
+ }
+ );
+
+export const REMOVE_SOURCE_ENGINE_BUTTON_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.sourceEngines.removeEngineButton.label',
+ { defaultMessage: 'Remove from meta engine' }
+);
+
+export const REMOVE_SOURCE_ENGINE_CONFIRM_DIALOGUE = (engineName: string) =>
+ i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.sourceEngines.removeEngineConfirmDialogue.description',
+ {
+ defaultMessage:
+ 'This will remove the engine, {engineName}, from this meta engine. All existing settings will be lost. Are you sure?',
+ values: { engineName },
+ }
+ );
+
+export const REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE = (engineName: string) =>
+ i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.souceEngines.removeSourceEngineSuccessMessage',
+ {
+ defaultMessage: 'Engine {engineName} has been removed from this meta engine.',
+ values: { engineName },
+ }
+ );
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx
index 4bf62de408a2b..8cfcaeec97b87 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx
@@ -5,52 +5,88 @@
* 2.0.
*/
-import '../../../__mocks__/shallow_useeffect.mock';
import { setMockActions, setMockValues } from '../../../__mocks__';
+import '../../../__mocks__/shallow_useeffect.mock';
import '../../__mocks__/engine_logic.mock';
import React from 'react';
-import { shallow } from 'enzyme';
+import { shallow, ShallowWrapper } from 'enzyme';
-import { EuiCodeBlock } from '@elastic/eui';
+import { EuiPageHeader } from '@elastic/eui';
import { Loading } from '../../../shared/loading';
+import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components';
+
import { SourceEngines } from '.';
-const MOCK_ACTIONS = {
- // SourceEnginesLogic
- fetchSourceEngines: jest.fn(),
-};
+describe('SourceEngines', () => {
+ const MOCK_ACTIONS = {
+ fetchIndexedEngines: jest.fn(),
+ fetchSourceEngines: jest.fn(),
+ };
-const MOCK_VALUES = {
- dataLoading: false,
- sourceEngines: [],
-};
+ const MOCK_VALUES = {
+ // AppLogic
+ myRole: {
+ canManageMetaEngineSourceEngines: true,
+ },
+ // SourceEnginesLogic
+ dataLoading: false,
+ isModalOpen: false,
+ };
-describe('SourceEngines', () => {
beforeEach(() => {
jest.clearAllMocks();
+ setMockValues(MOCK_VALUES);
setMockActions(MOCK_ACTIONS);
});
- describe('non-happy-path states', () => {
- it('renders a loading component before data has loaded', () => {
- setMockValues({ ...MOCK_VALUES, dataLoading: true });
- const wrapper = shallow();
+ it('renders and calls a function to initialize data', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(SourceEnginesTable)).toHaveLength(1);
+ expect(MOCK_ACTIONS.fetchIndexedEngines).toHaveBeenCalled();
+ expect(MOCK_ACTIONS.fetchSourceEngines).toHaveBeenCalled();
+ });
- expect(wrapper.find(Loading)).toHaveLength(1);
+ it('renders the add source engines modal', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ isModalOpen: true,
});
+ const wrapper = shallow();
+
+ expect(wrapper.find(AddSourceEnginesModal)).toHaveLength(1);
+ });
+
+ it('renders a loading component before data has loaded', () => {
+ setMockValues({ ...MOCK_VALUES, dataLoading: true });
+ const wrapper = shallow();
+
+ expect(wrapper.find(Loading)).toHaveLength(1);
});
- describe('happy-path states', () => {
- it('renders and calls a function to initialize data', () => {
- setMockValues(MOCK_VALUES);
+ describe('page actions', () => {
+ const getPageHeader = (wrapper: ShallowWrapper) =>
+ wrapper.find(EuiPageHeader).dive().children().dive();
+
+ it('contains a button to add source engines', () => {
+ const wrapper = shallow();
+ expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(1);
+ });
+
+ it('hides the add source engines button if the user does not have permissions', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ myRole: {
+ canManageMetaEngineSourceEngines: false,
+ },
+ });
const wrapper = shallow();
- expect(wrapper.find(EuiCodeBlock)).toHaveLength(1);
- expect(MOCK_ACTIONS.fetchSourceEngines).toHaveBeenCalled();
+ expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(0);
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx
index 0b68eb5fd2c2e..190c44c919020 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx
@@ -9,29 +9,27 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
-import { EuiCodeBlock, EuiPageHeader } from '@elastic/eui';
-
-import { i18n } from '@kbn/i18n';
+import { EuiPageHeader, EuiPageContent } from '@elastic/eui';
import { FlashMessages } from '../../../shared/flash_messages';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { Loading } from '../../../shared/loading';
+import { AppLogic } from '../../app_logic';
import { getEngineBreadcrumbs } from '../engine';
+import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components';
+import { SOURCE_ENGINES_TITLE } from './i18n';
import { SourceEnginesLogic } from './source_engines_logic';
-const SOURCE_ENGINES_TITLE = i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.souceEngines.title',
- {
- defaultMessage: 'Manage engines',
- }
-);
-
export const SourceEngines: React.FC = () => {
- const { fetchSourceEngines } = useActions(SourceEnginesLogic);
- const { dataLoading, sourceEngines } = useValues(SourceEnginesLogic);
+ const {
+ myRole: { canManageMetaEngineSourceEngines },
+ } = useValues(AppLogic);
+ const { fetchIndexedEngines, fetchSourceEngines } = useActions(SourceEnginesLogic);
+ const { dataLoading, isModalOpen } = useValues(SourceEnginesLogic);
useEffect(() => {
+ fetchIndexedEngines();
fetchSourceEngines();
}, []);
@@ -40,9 +38,15 @@ export const SourceEngines: React.FC = () => {
return (
<>
-
+ ] : []}
+ />
- {JSON.stringify(sourceEngines, null, 2)}
+
+
+ {isModalOpen && }
+
>
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts
index df1165620adc3..49886f1257a58 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts
@@ -6,129 +6,372 @@
*/
import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__';
+import { mockRecursivelyFetchEngines } from '../../__mocks__';
import '../../__mocks__/engine_logic.mock';
import { nextTick } from '@kbn/test/jest';
+import { EngineLogic } from '../engine';
import { EngineDetails } from '../engine/types';
import { SourceEnginesLogic } from './source_engines_logic';
-const DEFAULT_VALUES = {
- dataLoading: true,
- sourceEngines: [],
-};
-
describe('SourceEnginesLogic', () => {
const { http } = mockHttpValues;
const { mount } = new LogicMounter(SourceEnginesLogic);
- const { flashAPIErrors } = mockFlashMessageHelpers;
+ const { flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers;
+
+ const DEFAULT_VALUES = {
+ dataLoading: true,
+ modalLoading: false,
+ isModalOpen: false,
+ indexedEngines: [],
+ indexedEngineNames: [],
+ sourceEngines: [],
+ sourceEngineNames: [],
+ selectedEngineNamesToAdd: [],
+ selectableEngineNames: [],
+ };
beforeEach(() => {
jest.clearAllMocks();
- mount();
});
it('initializes with default values', () => {
+ mount();
expect(SourceEnginesLogic.values).toEqual(DEFAULT_VALUES);
});
- describe('setSourceEngines', () => {
- beforeEach(() => {
- SourceEnginesLogic.actions.onSourceEnginesFetch([
- { name: 'source-engine-1' },
- { name: 'source-engine-2' },
- ] as EngineDetails[]);
+ describe('actions', () => {
+ describe('closeModal', () => {
+ it('sets isModalOpen and modalLoading to false', () => {
+ mount({
+ isModalOpen: true,
+ modalLoading: true,
+ });
+
+ SourceEnginesLogic.actions.closeModal();
+
+ expect(SourceEnginesLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ isModalOpen: false,
+ modalLoading: false,
+ });
+ });
});
- it('sets the source engines', () => {
- expect(SourceEnginesLogic.values.sourceEngines).toEqual([
- { name: 'source-engine-1' },
- { name: 'source-engine-2' },
- ]);
+ describe('openModal', () => {
+ it('sets isModalOpen to true', () => {
+ mount({
+ isModalOpen: false,
+ });
+
+ SourceEnginesLogic.actions.openModal();
+
+ expect(SourceEnginesLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ isModalOpen: true,
+ });
+ });
});
- it('sets dataLoading to false', () => {
- expect(SourceEnginesLogic.values.dataLoading).toEqual(false);
+ describe('onAddEnginesSelection', () => {
+ it('sets selectedEngineNamesToAdd to the specified value', () => {
+ mount();
+
+ SourceEnginesLogic.actions.onAddEnginesSelection(['source-engine-1', 'source-engine-2']);
+
+ expect(SourceEnginesLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ selectedEngineNamesToAdd: ['source-engine-1', 'source-engine-2'],
+ });
+ });
+ });
+
+ describe('setIndexedEngines', () => {
+ it('sets indexedEngines to the specified value', () => {
+ mount();
+
+ SourceEnginesLogic.actions.setIndexedEngines([
+ { name: 'source-engine-1' },
+ { name: 'source-engine-2' },
+ ] as EngineDetails[]);
+
+ expect(SourceEnginesLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ indexedEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }],
+ // Selectors
+ indexedEngineNames: ['source-engine-1', 'source-engine-2'],
+ selectableEngineNames: ['source-engine-1', 'source-engine-2'],
+ });
+ });
+ });
+
+ describe('onSourceEnginesFetch', () => {
+ it('sets sourceEngines to the specified value and dataLoading to false', () => {
+ mount();
+
+ SourceEnginesLogic.actions.onSourceEnginesFetch([
+ { name: 'source-engine-1' },
+ { name: 'source-engine-2' },
+ ] as EngineDetails[]);
+
+ expect(SourceEnginesLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ dataLoading: false,
+ sourceEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }],
+ // Selectors
+ sourceEngineNames: ['source-engine-1', 'source-engine-2'],
+ });
+ });
+ });
+
+ describe('onSourceEnginesAdd', () => {
+ it('adds to the existing sourceEngines', () => {
+ mount({
+ sourceEngines: [
+ { name: 'source-engine-1' },
+ { name: 'source-engine-2' },
+ ] as EngineDetails[],
+ });
+
+ SourceEnginesLogic.actions.onSourceEnginesAdd([
+ { name: 'source-engine-3' },
+ { name: 'source-engine-4' },
+ ] as EngineDetails[]);
+
+ expect(SourceEnginesLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ sourceEngines: [
+ { name: 'source-engine-1' },
+ { name: 'source-engine-2' },
+ { name: 'source-engine-3' },
+ { name: 'source-engine-4' },
+ ],
+ // Selectors
+ sourceEngineNames: [
+ 'source-engine-1',
+ 'source-engine-2',
+ 'source-engine-3',
+ 'source-engine-4',
+ ],
+ });
+ });
+ });
+
+ describe('onSourceEngineRemove', () => {
+ it('removes an item from the existing sourceEngines', () => {
+ mount({
+ sourceEngines: [
+ { name: 'source-engine-1' },
+ { name: 'source-engine-2' },
+ { name: 'source-engine-3' },
+ ] as EngineDetails[],
+ });
+
+ SourceEnginesLogic.actions.onSourceEngineRemove('source-engine-2');
+
+ expect(SourceEnginesLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ sourceEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-3' }],
+ // Selectors
+ sourceEngineNames: ['source-engine-1', 'source-engine-3'],
+ });
+ });
});
});
- describe('fetchSourceEngines', () => {
- it('calls addSourceEngines and displayRow when it has retrieved all pages', async () => {
- http.get.mockReturnValueOnce(
- Promise.resolve({
- meta: {
- page: {
- total_pages: 1,
- },
- },
- results: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }],
- })
- );
- jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesFetch');
-
- SourceEnginesLogic.actions.fetchSourceEngines();
- await nextTick();
-
- expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/source_engines', {
- query: {
- 'page[current]': 1,
- 'page[size]': 25,
- },
- });
- expect(SourceEnginesLogic.actions.onSourceEnginesFetch).toHaveBeenCalledWith([
- { name: 'source-engine-1' },
- { name: 'source-engine-2' },
- ]);
+ describe('selectors', () => {
+ describe('indexedEngineNames', () => {
+ it('returns a flat array of `indexedEngine.name`s', () => {
+ mount({
+ indexedEngines: [{ name: 'a' }, { name: 'b' }, { name: 'c' }],
+ });
+
+ expect(SourceEnginesLogic.values.indexedEngineNames).toEqual(['a', 'b', 'c']);
+ });
});
- it('display a flash message on error', async () => {
- http.get.mockReturnValueOnce(Promise.reject());
- mount();
+ describe('sourceEngineNames', () => {
+ it('returns a flat array of `sourceEngine.name`s', () => {
+ mount({
+ sourceEngines: [{ name: 'd' }, { name: 'e' }],
+ });
+
+ expect(SourceEnginesLogic.values.sourceEngineNames).toEqual(['d', 'e']);
+ });
+ });
- SourceEnginesLogic.actions.fetchSourceEngines();
- await nextTick();
+ describe('selectableEngineNames', () => {
+ it('returns a flat list of indexedEngineNames that are not already in sourceEngineNames', () => {
+ mount({
+ indexedEngines: [{ name: 'a' }, { name: 'b' }, { name: 'c' }],
+ sourceEngines: [{ name: 'a' }, { name: 'b' }],
+ });
- expect(flashAPIErrors).toHaveBeenCalledTimes(1);
+ expect(SourceEnginesLogic.values.selectableEngineNames).toEqual(['c']);
+ });
});
+ });
+
+ describe('listeners', () => {
+ describe('fetchSourceEngines', () => {
+ it('calls onSourceEnginesFetch with all recursively fetched engines', () => {
+ jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesFetch');
- it('recursively fetches a number of pages', async () => {
- mount();
- jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesFetch');
-
- // First page
- http.get.mockReturnValueOnce(
- Promise.resolve({
- meta: {
- page: {
- total_pages: 2,
- },
- },
- results: [{ name: 'source-engine-1' }],
- })
- );
-
- // Second and final page
- http.get.mockReturnValueOnce(
- Promise.resolve({
- meta: {
- page: {
- total_pages: 2,
- },
- },
- results: [{ name: 'source-engine-2' }],
- })
- );
-
- SourceEnginesLogic.actions.fetchSourceEngines();
- await nextTick();
-
- expect(SourceEnginesLogic.actions.onSourceEnginesFetch).toHaveBeenCalledWith([
- // First page
- { name: 'source-engine-1' },
- // Second and final page
- { name: 'source-engine-2' },
- ]);
+ SourceEnginesLogic.actions.fetchSourceEngines();
+
+ expect(mockRecursivelyFetchEngines).toHaveBeenCalledWith(
+ expect.objectContaining({
+ endpoint: '/api/app_search/engines/some-engine/source_engines',
+ })
+ );
+ expect(SourceEnginesLogic.actions.onSourceEnginesFetch).toHaveBeenCalledWith([
+ { name: 'source-engine-1' },
+ { name: 'source-engine-2' },
+ ]);
+ });
+ });
+
+ describe('fetchIndexedEngines', () => {
+ it('calls setIndexedEngines with all recursively fetched engines', () => {
+ jest.spyOn(SourceEnginesLogic.actions, 'setIndexedEngines');
+
+ SourceEnginesLogic.actions.fetchIndexedEngines();
+
+ expect(mockRecursivelyFetchEngines).toHaveBeenCalledWith(
+ expect.objectContaining({
+ endpoint: '/api/app_search/engines',
+ query: { type: 'indexed' },
+ })
+ );
+ expect(SourceEnginesLogic.actions.setIndexedEngines).toHaveBeenCalledWith([
+ { name: 'source-engine-1' },
+ { name: 'source-engine-2' },
+ ]);
+ });
+ });
+
+ describe('addSourceEngines', () => {
+ it('sets modalLoading to true', () => {
+ mount({ modalLoading: false });
+
+ SourceEnginesLogic.actions.addSourceEngines([]);
+
+ expect(SourceEnginesLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ modalLoading: true,
+ });
+ });
+
+ describe('on success', () => {
+ beforeEach(() => {
+ http.post.mockReturnValue(Promise.resolve());
+ mount({
+ indexedEngines: [{ name: 'source-engine-3' }, { name: 'source-engine-4' }],
+ });
+ });
+
+ it('calls the bulk endpoint, adds source engines to state, and shows a success message', async () => {
+ jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesAdd');
+
+ SourceEnginesLogic.actions.addSourceEngines(['source-engine-3', 'source-engine-4']);
+ await nextTick();
+
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/app_search/engines/some-engine/source_engines/bulk_create',
+ {
+ body: JSON.stringify({ source_engine_slugs: ['source-engine-3', 'source-engine-4'] }),
+ }
+ );
+ expect(SourceEnginesLogic.actions.onSourceEnginesAdd).toHaveBeenCalledWith([
+ { name: 'source-engine-3' },
+ { name: 'source-engine-4' },
+ ]);
+ expect(setSuccessMessage).toHaveBeenCalledWith(
+ '2 engines have been added to this meta engine.'
+ );
+ });
+
+ it('re-initializes the engine and closes the modal', async () => {
+ jest.spyOn(EngineLogic.actions, 'initializeEngine');
+ jest.spyOn(SourceEnginesLogic.actions, 'closeModal');
+
+ SourceEnginesLogic.actions.addSourceEngines([]);
+ await nextTick();
+
+ expect(EngineLogic.actions.initializeEngine).toHaveBeenCalled();
+ expect(SourceEnginesLogic.actions.closeModal).toHaveBeenCalled();
+ });
+ });
+
+ describe('on error', () => {
+ beforeEach(() => {
+ http.post.mockReturnValue(Promise.reject());
+ mount();
+ });
+
+ it('flashes errors and closes the modal', async () => {
+ jest.spyOn(SourceEnginesLogic.actions, 'closeModal');
+
+ SourceEnginesLogic.actions.addSourceEngines([]);
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledTimes(1);
+ expect(SourceEnginesLogic.actions.closeModal).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('removeSourceEngine', () => {
+ describe('on success', () => {
+ beforeEach(() => {
+ http.delete.mockReturnValue(Promise.resolve());
+ mount();
+ });
+
+ it('calls the delete endpoint and removes source engines from state', async () => {
+ jest.spyOn(SourceEnginesLogic.actions, 'onSourceEngineRemove');
+
+ SourceEnginesLogic.actions.removeSourceEngine('source-engine-2');
+ await nextTick();
+
+ expect(http.delete).toHaveBeenCalledWith(
+ '/api/app_search/engines/some-engine/source_engines/source-engine-2'
+ );
+ expect(SourceEnginesLogic.actions.onSourceEngineRemove).toHaveBeenCalledWith(
+ 'source-engine-2'
+ );
+ });
+
+ it('shows a success message', async () => {
+ SourceEnginesLogic.actions.removeSourceEngine('source-engine-2');
+ await nextTick();
+
+ expect(setSuccessMessage).toHaveBeenCalledWith(
+ 'Engine source-engine-2 has been removed from this meta engine.'
+ );
+ });
+
+ it('re-initializes the engine', async () => {
+ jest.spyOn(EngineLogic.actions, 'initializeEngine');
+
+ SourceEnginesLogic.actions.removeSourceEngine('source-engine-2');
+ await nextTick();
+
+ expect(EngineLogic.actions.initializeEngine).toHaveBeenCalledWith();
+ });
+ });
+
+ it('displays a flash message on error', async () => {
+ http.delete.mockReturnValueOnce(Promise.reject());
+ mount();
+
+ SourceEnginesLogic.actions.removeSourceEngine('source-engine-2');
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledTimes(1);
+ });
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts
index b8a5c7c359518..c10f11a7de327 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts
@@ -4,24 +4,47 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
import { kea, MakeLogicType } from 'kea';
-import { flashAPIErrors } from '../../../shared/flash_messages';
+import { flashAPIErrors, setSuccessMessage } from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
+import { recursivelyFetchEngines } from '../../utils/recursively_fetch_engines';
import { EngineLogic } from '../engine';
import { EngineDetails } from '../engine/types';
-import { EnginesAPIResponse } from '../engines/types';
-interface SourceEnginesLogicValues {
+import { ADD_SOURCE_ENGINES_SUCCESS_MESSAGE, REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE } from './i18n';
+
+export interface SourceEnginesLogicValues {
dataLoading: boolean;
+ modalLoading: boolean;
+ isModalOpen: boolean;
+ indexedEngines: EngineDetails[];
+ indexedEngineNames: string[];
sourceEngines: EngineDetails[];
+ sourceEngineNames: string[];
+ selectableEngineNames: string[];
+ selectedEngineNamesToAdd: string[];
}
interface SourceEnginesLogicActions {
+ addSourceEngines: (sourceEngineNames: string[]) => { sourceEngineNames: string[] };
+ fetchIndexedEngines: () => void;
fetchSourceEngines: () => void;
+ onSourceEngineRemove: (sourceEngineNameToRemove: string) => { sourceEngineNameToRemove: string };
+ onSourceEnginesAdd: (
+ sourceEnginesToAdd: EngineDetails[]
+ ) => { sourceEnginesToAdd: EngineDetails[] };
onSourceEnginesFetch: (
sourceEngines: SourceEnginesLogicValues['sourceEngines']
) => { sourceEngines: SourceEnginesLogicValues['sourceEngines'] };
+ removeSourceEngine: (sourceEngineName: string) => { sourceEngineName: string };
+ setIndexedEngines: (indexedEngines: EngineDetails[]) => { indexedEngines: EngineDetails[] };
+ openModal: () => void;
+ closeModal: () => void;
+ onAddEnginesSelection: (
+ selectedEngineNamesToAdd: string[]
+ ) => { selectedEngineNamesToAdd: string[] };
}
export const SourceEnginesLogic = kea<
@@ -29,8 +52,17 @@ export const SourceEnginesLogic = kea<
>({
path: ['enterprise_search', 'app_search', 'source_engines_logic'],
actions: () => ({
+ addSourceEngines: (sourceEngineNames) => ({ sourceEngineNames }),
+ fetchIndexedEngines: true,
fetchSourceEngines: true,
+ onSourceEngineRemove: (sourceEngineNameToRemove) => ({ sourceEngineNameToRemove }),
+ onSourceEnginesAdd: (sourceEnginesToAdd) => ({ sourceEnginesToAdd }),
onSourceEnginesFetch: (sourceEngines) => ({ sourceEngines }),
+ removeSourceEngine: (sourceEngineName) => ({ sourceEngineName }),
+ setIndexedEngines: (indexedEngines) => ({ indexedEngines }),
+ openModal: true,
+ closeModal: true,
+ onAddEnginesSelection: (selectedEngineNamesToAdd) => ({ selectedEngineNamesToAdd }),
}),
reducers: () => ({
dataLoading: [
@@ -39,47 +71,119 @@ export const SourceEnginesLogic = kea<
onSourceEnginesFetch: () => false,
},
],
+ modalLoading: [
+ false,
+ {
+ addSourceEngines: () => true,
+ closeModal: () => false,
+ },
+ ],
+ isModalOpen: [
+ false,
+ {
+ openModal: () => true,
+ closeModal: () => false,
+ },
+ ],
+ indexedEngines: [
+ [],
+ {
+ setIndexedEngines: (_, { indexedEngines }) => indexedEngines,
+ },
+ ],
+ selectedEngineNamesToAdd: [
+ [],
+ {
+ closeModal: () => [],
+ onAddEnginesSelection: (_, { selectedEngineNamesToAdd }) => selectedEngineNamesToAdd,
+ },
+ ],
sourceEngines: [
[],
{
+ onSourceEnginesAdd: (sourceEngines, { sourceEnginesToAdd }) => [
+ ...sourceEngines,
+ ...sourceEnginesToAdd,
+ ],
onSourceEnginesFetch: (_, { sourceEngines }) => sourceEngines,
+ onSourceEngineRemove: (sourceEngines, { sourceEngineNameToRemove }) =>
+ sourceEngines.filter((sourceEngine) => sourceEngine.name !== sourceEngineNameToRemove),
},
],
}),
- listeners: ({ actions }) => ({
- fetchSourceEngines: () => {
+ selectors: {
+ indexedEngineNames: [
+ (selectors) => [selectors.indexedEngines],
+ (indexedEngines) => indexedEngines.map((engine: EngineDetails) => engine.name),
+ ],
+ sourceEngineNames: [
+ (selectors) => [selectors.sourceEngines],
+ (sourceEngines) => sourceEngines.map((engine: EngineDetails) => engine.name),
+ ],
+ selectableEngineNames: [
+ (selectors) => [selectors.indexedEngineNames, selectors.sourceEngineNames],
+ (indexedEngineNames, sourceEngineNames) =>
+ indexedEngineNames.filter((engineName: string) => !sourceEngineNames.includes(engineName)),
+ ],
+ },
+ listeners: ({ actions, values }) => ({
+ addSourceEngines: async ({ sourceEngineNames }) => {
const { http } = HttpLogic.values;
const { engineName } = EngineLogic.values;
- let enginesAccumulator: EngineDetails[] = [];
+ try {
+ await http.post(`/api/app_search/engines/${engineName}/source_engines/bulk_create`, {
+ body: JSON.stringify({
+ source_engine_slugs: sourceEngineNames,
+ }),
+ });
+
+ const sourceEnginesToAdd = values.indexedEngines.filter(({ name }) =>
+ sourceEngineNames.includes(name)
+ );
+
+ actions.onSourceEnginesAdd(sourceEnginesToAdd);
+ setSuccessMessage(ADD_SOURCE_ENGINES_SUCCESS_MESSAGE(sourceEngineNames));
+ EngineLogic.actions.initializeEngine();
+ } catch (e) {
+ flashAPIErrors(e);
+ } finally {
+ actions.closeModal();
+ }
+ },
+ fetchSourceEngines: () => {
+ const { engineName } = EngineLogic.values;
- // We need to recursively fetch all source engines because we put the data
- // into an EuiInMemoryTable to enable searching
- const recursiveFetchSourceEngines = async (page = 1) => {
- try {
- const { meta, results }: EnginesAPIResponse = await http.get(
- `/api/app_search/engines/${engineName}/source_engines`,
- {
- query: {
- 'page[current]': page,
- 'page[size]': 25,
- },
- }
- );
+ recursivelyFetchEngines({
+ endpoint: `/api/app_search/engines/${engineName}/source_engines`,
+ onComplete: (engines) => actions.onSourceEnginesFetch(engines),
+ });
+ },
+ fetchIndexedEngines: () => {
+ recursivelyFetchEngines({
+ endpoint: '/api/app_search/engines',
+ onComplete: (engines) => actions.setIndexedEngines(engines),
+ query: { type: 'indexed' },
+ });
+ },
+ removeSourceEngine: async ({ sourceEngineName }) => {
+ const { http } = HttpLogic.values;
+ const { engineName } = EngineLogic.values;
- enginesAccumulator = [...enginesAccumulator, ...results];
+ try {
+ await http.delete(
+ `/api/app_search/engines/${engineName}/source_engines/${sourceEngineName}`
+ );
- if (page >= meta.page.total_pages) {
- actions.onSourceEnginesFetch(enginesAccumulator);
- } else {
- recursiveFetchSourceEngines(page + 1);
- }
- } catch (e) {
- flashAPIErrors(e);
- }
- };
+ actions.onSourceEngineRemove(sourceEngineName);
+ setSuccessMessage(REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE(sourceEngineName));
- recursiveFetchSourceEngines();
+ // Changing source engines can change schema conflicts and invalid boosts,
+ // so we re-initialize the engine to re-fetch that data
+ EngineLogic.actions.initializeEngine(); //
+ } catch (e) {
+ flashAPIErrors(e);
+ }
},
}),
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts
new file mode 100644
index 0000000000000..104f98e45a5f5
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts
@@ -0,0 +1,108 @@
+/*
+ * 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 { mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__';
+
+import { nextTick } from '@kbn/test/jest';
+
+import { recursivelyFetchEngines } from './';
+
+describe('recursivelyFetchEngines', () => {
+ const { http } = mockHttpValues;
+ const { flashAPIErrors } = mockFlashMessageHelpers;
+
+ const MOCK_PAGE_1 = {
+ meta: {
+ page: { current: 1, total_pages: 3 },
+ },
+ results: [{ name: 'source-engine-1' }],
+ };
+ const MOCK_PAGE_2 = {
+ meta: {
+ page: { current: 2, total_pages: 3 },
+ },
+ results: [{ name: 'source-engine-2' }],
+ };
+ const MOCK_PAGE_3 = {
+ meta: {
+ page: { current: 3, total_pages: 3 },
+ },
+ results: [{ name: 'source-engine-3' }],
+ };
+ const MOCK_CALLBACK = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('recursively calls the passed API endpoint and returns all engines to the onComplete callback', async () => {
+ http.get
+ .mockReturnValueOnce(Promise.resolve(MOCK_PAGE_1))
+ .mockReturnValueOnce(Promise.resolve(MOCK_PAGE_2))
+ .mockReturnValueOnce(Promise.resolve(MOCK_PAGE_3));
+
+ recursivelyFetchEngines({
+ endpoint: '/api/app_search/engines/some-engine/source_engines',
+ onComplete: MOCK_CALLBACK,
+ });
+ await nextTick();
+
+ expect(http.get).toHaveBeenCalledTimes(3); // Called once for each page
+ expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/source_engines', {
+ query: {
+ 'page[current]': 1,
+ 'page[size]': 25,
+ },
+ });
+
+ expect(MOCK_CALLBACK).toHaveBeenCalledWith([
+ { name: 'source-engine-1' },
+ { name: 'source-engine-2' },
+ { name: 'source-engine-3' },
+ ]);
+ });
+
+ it('passes optional query params', () => {
+ recursivelyFetchEngines({
+ endpoint: '/api/app_search/engines/some-engine/engines',
+ onComplete: MOCK_CALLBACK,
+ query: { type: 'indexed' },
+ });
+
+ expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/engines', {
+ query: {
+ 'page[current]': 1,
+ 'page[size]': 25,
+ type: 'indexed',
+ },
+ });
+ });
+
+ it('passes optional custom page sizes', () => {
+ recursivelyFetchEngines({
+ endpoint: '/over_9000',
+ onComplete: MOCK_CALLBACK,
+ pageSize: 9001,
+ });
+
+ expect(http.get).toHaveBeenCalledWith('/over_9000', {
+ query: {
+ 'page[current]': 1,
+ 'page[size]': 9001,
+ },
+ });
+ });
+
+ it('handles errors', async () => {
+ http.get.mockReturnValueOnce(Promise.reject('error'));
+
+ recursivelyFetchEngines({ endpoint: '/error', onComplete: MOCK_CALLBACK });
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledWith('error');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts
new file mode 100644
index 0000000000000..797e89bd68b69
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { flashAPIErrors } from '../../../shared/flash_messages';
+import { HttpLogic } from '../../../shared/http';
+
+import { EngineDetails } from '../../components/engine/types';
+import { EnginesAPIResponse } from '../../components/engines/types';
+
+interface Params {
+ endpoint: string;
+ onComplete: (engines: EngineDetails[]) => void;
+ query?: object;
+ pageSize?: number;
+}
+
+export const recursivelyFetchEngines = ({
+ endpoint,
+ onComplete,
+ query = {},
+ pageSize = 25,
+}: Params) => {
+ const { http } = HttpLogic.values;
+
+ let enginesAccumulator: EngineDetails[] = [];
+
+ const fetchEngines = async (page = 1) => {
+ try {
+ const { meta, results }: EnginesAPIResponse = await http.get(endpoint, {
+ query: {
+ 'page[current]': page,
+ 'page[size]': pageSize,
+ ...query,
+ },
+ });
+
+ enginesAccumulator = [...enginesAccumulator, ...results];
+
+ if (page >= meta.page.total_pages) {
+ onComplete(enginesAccumulator);
+ } else {
+ fetchEngines(page + 1);
+ }
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ };
+
+ fetchEngines();
+};
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts
index bc4259fa37889..c653cad5c1c0d 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts
@@ -259,47 +259,4 @@ describe('engine routes', () => {
});
});
});
-
- describe('GET /api/app_search/engines/{name}/source_engines', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/app_search/engines/{name}/source_engines',
- });
-
- registerEnginesRoutes({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('validates correctly with name', () => {
- const request = { params: { name: 'test-engine' } };
- mockRouter.shouldValidate(request);
- });
-
- it('fails validation without name', () => {
- const request = { params: {} };
- mockRouter.shouldThrow(request);
- });
-
- it('fails validation with a non-string name', () => {
- const request = { params: { name: 1 } };
- mockRouter.shouldThrow(request);
- });
-
- it('fails validation with missing query params', () => {
- const request = { query: {} };
- mockRouter.shouldThrow(request);
- });
-
- it('creates a request to enterprise search', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/as/engines/:name/source_engines',
- });
- });
- });
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts
index f6e9d30dd0ade..77b055add7d79 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts
@@ -95,21 +95,4 @@ export function registerEnginesRoutes({
path: '/as/engines/:name/overview_metrics',
})
);
- router.get(
- {
- path: '/api/app_search/engines/{name}/source_engines',
- validate: {
- params: schema.object({
- name: schema.string(),
- }),
- query: schema.object({
- 'page[current]': schema.number(),
- 'page[size]': schema.number(),
- }),
- },
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/as/engines/:name/source_engines',
- })
- );
}
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts
index 99aaaeeec38b3..18de4580318a2 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts
@@ -20,6 +20,7 @@ import { registerSchemaRoutes } from './schema';
import { registerSearchSettingsRoutes } from './search_settings';
import { registerSearchUIRoutes } from './search_ui';
import { registerSettingsRoutes } from './settings';
+import { registerSourceEnginesRoutes } from './source_engines';
import { registerSynonymsRoutes } from './synonyms';
export const registerAppSearchRoutes = (dependencies: RouteDependencies) => {
@@ -30,6 +31,7 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => {
registerDocumentsRoutes(dependencies);
registerDocumentRoutes(dependencies);
registerSchemaRoutes(dependencies);
+ registerSourceEnginesRoutes(dependencies);
registerCurationsRoutes(dependencies);
registerSynonymsRoutes(dependencies);
registerSearchSettingsRoutes(dependencies);
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts
new file mode 100644
index 0000000000000..5b51048067c00
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts
@@ -0,0 +1,151 @@
+/*
+ * 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 { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
+
+import { registerSourceEnginesRoutes } from './source_engines';
+
+describe('source engine routes', () => {
+ describe('GET /api/app_search/engines/{name}/source_engines', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'get',
+ path: '/api/app_search/engines/{name}/source_engines',
+ });
+
+ registerSourceEnginesRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('validates correctly with name', () => {
+ const request = { params: { name: 'test-engine' } };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('fails validation without name', () => {
+ const request = { params: {} };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('fails validation with a non-string name', () => {
+ const request = { params: { name: 1 } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('fails validation with missing query params', () => {
+ const request = { query: {} };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('creates a request to enterprise search', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/as/engines/:name/source_engines',
+ });
+ });
+ });
+
+ describe('POST /api/app_search/engines/{name}/source_engines/bulk_create', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'post',
+ path: '/api/app_search/engines/{name}/source_engines/bulk_create',
+ });
+
+ registerSourceEnginesRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('validates correctly with name', () => {
+ const request = { params: { name: 'test-engine' }, body: { source_engine_slugs: [] } };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('fails validation without name', () => {
+ const request = { params: {}, body: { source_engine_slugs: [] } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('fails validation with a non-string name', () => {
+ const request = { params: { name: 1 }, body: { source_engine_slugs: [] } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('fails validation with missing query params', () => {
+ const request = { params: { name: 'test-engine' }, body: {} };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('creates a request to enterprise search', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/as/engines/:name/source_engines/bulk_create',
+ });
+ });
+ });
+
+ describe('DELETE /api/app_search/engines/{name}/source_engines/{source_engine_name}', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'delete',
+ path: '/api/app_search/engines/{name}/source_engines/{source_engine_name}',
+ });
+
+ registerSourceEnginesRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('validates correctly with name and source_engine_name', () => {
+ const request = { params: { name: 'test-engine', source_engine_name: 'source-engine' } };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('fails validation without name', () => {
+ const request = { params: { source_engine_name: 'source-engine' } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('fails validation with a non-string name', () => {
+ const request = { params: { name: 1, source_engine_name: 'source-engine' } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('fails validation without source_engine_name', () => {
+ const request = { params: { name: 'test-engine' } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('fails validation with a non-string source_engine_name', () => {
+ const request = { params: { name: 'test-engine', source_engine_name: 1 } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('fails validation with missing query params', () => {
+ const request = { query: {} };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('creates a request to enterprise search', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/as/engines/:name/source_engines/:source_engine_name',
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts
new file mode 100644
index 0000000000000..8e55b0e6f1ac6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+
+import { RouteDependencies } from '../../plugin';
+
+export function registerSourceEnginesRoutes({
+ router,
+ enterpriseSearchRequestHandler,
+}: RouteDependencies) {
+ router.get(
+ {
+ path: '/api/app_search/engines/{name}/source_engines',
+ validate: {
+ params: schema.object({
+ name: schema.string(),
+ }),
+ query: schema.object({
+ 'page[current]': schema.number(),
+ 'page[size]': schema.number(),
+ }),
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/as/engines/:name/source_engines',
+ })
+ );
+
+ router.post(
+ {
+ path: '/api/app_search/engines/{name}/source_engines/bulk_create',
+ validate: {
+ params: schema.object({
+ name: schema.string(),
+ }),
+ body: schema.object({
+ source_engine_slugs: schema.arrayOf(schema.string()),
+ }),
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/as/engines/:name/source_engines/bulk_create',
+ })
+ );
+
+ router.delete(
+ {
+ path: '/api/app_search/engines/{name}/source_engines/{source_engine_name}',
+ validate: {
+ params: schema.object({
+ name: schema.string(),
+ source_engine_name: schema.string(),
+ }),
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/as/engines/:name/source_engines/:source_engine_name',
+ })
+ );
+}
From 5e410f5d863bfcdbc66768e31fc10af68bd48dd9 Mon Sep 17 00:00:00 2001
From: Dominique Clarke
Date: Sun, 16 May 2021 18:09:12 -0400
Subject: [PATCH 09/11] [Uptime] [Synthetics Integration] update tls passphrase
and http password field to use EuiFieldPassword (#100162)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../public/components/fleet_package/http_advanced_fields.tsx | 3 ++-
.../uptime/public/components/fleet_package/tls_fields.tsx | 4 ++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx
index 5cc1dd12ef961..7ab6c81fbf162 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx
@@ -17,6 +17,7 @@ import {
EuiDescribedFormGroup,
EuiCheckbox,
EuiSpacer,
+ EuiFieldPassword,
} from '@elastic/eui';
import { useHTTPAdvancedFieldsContext } from './contexts';
@@ -110,7 +111,7 @@ export const HTTPAdvancedFields = memo(({ validate }) => {
/>
}
>
-
handleInputChange({
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx
index e01d3d59175a4..de8879ec3a819 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx
@@ -13,12 +13,12 @@ import {
EuiComboBox,
EuiComboBoxOptionOption,
EuiFormRow,
- EuiFieldText,
EuiTextArea,
EuiFormFieldset,
EuiSelect,
EuiScreenReaderOnly,
EuiSpacer,
+ EuiFieldPassword,
} from '@elastic/eui';
import { useTLSFieldsContext } from './contexts';
@@ -333,7 +333,7 @@ export const TLSFields: React.FunctionComponent<{
}
labelAppend={}
>
- {
const value = event.target.value;
From d8a2f8f95c05a9b94d1969487433539954fd12ef Mon Sep 17 00:00:00 2001
From: Mikhail Shustov
Date: Mon, 17 May 2021 11:46:57 +0200
Subject: [PATCH 10/11] Improve migration perf (#99773)
* Do not clone state, use TypeCheck it's not mutated
* do not recreate context for every migration
* use more optional semver check
* update SavedObjectMigrationContext type
* add a test model returns new state object
* update docs
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
...text.converttomultinamespacetypeversion.md | 2 +-
...-server.savedobjectmigrationcontext.log.md | 2 +-
...objectmigrationcontext.migrationversion.md | 2 +-
.../migrations/core/document_migrator.ts | 11 ++++----
.../server/saved_objects/migrations/types.ts | 6 ++---
.../saved_objects/migrationsv2/model.test.ts | 25 +++++++++++++++++++
.../saved_objects/migrationsv2/model.ts | 4 +--
.../saved_objects/migrationsv2/types.ts | 5 ++--
src/core/server/server.api.md | 6 ++---
.../common/saved_dashboard_references.ts | 5 ++--
10 files changed, 47 insertions(+), 21 deletions(-)
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md
index 2a30693f4da84..9fe43a2f3f477 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md
@@ -9,5 +9,5 @@ The version in which this object type is being converted to a multi-namespace ty
Signature:
```typescript
-convertToMultiNamespaceTypeVersion?: string;
+readonly convertToMultiNamespaceTypeVersion?: string;
```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md
index a1b3378afc53b..20a0e99275a39 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md
@@ -9,5 +9,5 @@ logger instance to be used by the migration handler
Signature:
```typescript
-log: SavedObjectsMigrationLogger;
+readonly log: SavedObjectsMigrationLogger;
```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md
index 7b20ae41048f6..a1c2717e6e4a0 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md
@@ -9,5 +9,5 @@ The migration version that this migration function is defined for
Signature:
```typescript
-migrationVersion: string;
+readonly migrationVersion: string;
```
diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts
index f30cfc53018db..c96de6ebbfcdd 100644
--- a/src/core/server/saved_objects/migrations/core/document_migrator.ts
+++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts
@@ -661,13 +661,14 @@ function wrapWithTry(
migrationFn: SavedObjectMigrationFn,
log: Logger
) {
+ const context = Object.freeze({
+ log: new MigrationLogger(log),
+ migrationVersion: version,
+ convertToMultiNamespaceTypeVersion: type.convertToMultiNamespaceTypeVersion,
+ });
+
return function tryTransformDoc(doc: SavedObjectUnsanitizedDoc) {
try {
- const context = {
- log: new MigrationLogger(log),
- migrationVersion: version,
- convertToMultiNamespaceTypeVersion: type.convertToMultiNamespaceTypeVersion,
- };
const result = migrationFn(doc, context);
// A basic sanity check to help migration authors detect basic errors
diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts
index 619a7f85a327b..570315e780ebe 100644
--- a/src/core/server/saved_objects/migrations/types.ts
+++ b/src/core/server/saved_objects/migrations/types.ts
@@ -56,15 +56,15 @@ export interface SavedObjectMigrationContext {
/**
* logger instance to be used by the migration handler
*/
- log: SavedObjectsMigrationLogger;
+ readonly log: SavedObjectsMigrationLogger;
/**
* The migration version that this migration function is defined for
*/
- migrationVersion: string;
+ readonly migrationVersion: string;
/**
* The version in which this object type is being converted to a multi-namespace type
*/
- convertToMultiNamespaceTypeVersion?: string;
+ readonly convertToMultiNamespaceTypeVersion?: string;
}
/**
diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts
index adeb78e568af3..7a47e58f1947c 100644
--- a/src/core/server/saved_objects/migrationsv2/model.test.ts
+++ b/src/core/server/saved_objects/migrationsv2/model.test.ts
@@ -198,6 +198,31 @@ describe('migrations v2 model', () => {
});
describe('model transitions from', () => {
+ it('transition returns new state', () => {
+ const initState: State = {
+ ...baseState,
+ controlState: 'INIT',
+ currentAlias: '.kibana',
+ versionAlias: '.kibana_7.11.0',
+ versionIndex: '.kibana_7.11.0_001',
+ };
+
+ const res: ResponseType<'INIT'> = Either.right({
+ '.kibana_7.11.0_001': {
+ aliases: {
+ '.kibana': {},
+ '.kibana_7.11.0': {},
+ },
+ mappings: {
+ properties: {},
+ },
+ settings: {},
+ },
+ });
+ const newState = model(initState, res);
+ expect(newState).not.toBe(initState);
+ });
+
describe('INIT', () => {
const initState: State = {
...baseState,
diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts
index 3ef3cb4f83b6f..f4185225ae073 100644
--- a/src/core/server/saved_objects/migrationsv2/model.ts
+++ b/src/core/server/saved_objects/migrationsv2/model.ts
@@ -9,7 +9,7 @@
import { gt, valid } from 'semver';
import * as Either from 'fp-ts/lib/Either';
import * as Option from 'fp-ts/lib/Option';
-import { cloneDeep } from 'lodash';
+
import { AliasAction, FetchIndexResponse, isLeftTypeof, RetryableEsClientError } from './actions';
import { AllActionStates, InitState, State } from './types';
import { IndexMapping } from '../mappings';
@@ -187,7 +187,7 @@ export const model = (currentState: State, resW: ResponseType):
// control state using:
// `const res = resW as ResponseType;`
- let stateP: State = cloneDeep(currentState);
+ let stateP: State = currentState;
// Handle retryable_es_client_errors. Other left values need to be handled
// by the control state specific code below.
diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts
index e3e52212d56cb..adcd2ad32fd24 100644
--- a/src/core/server/saved_objects/migrationsv2/types.ts
+++ b/src/core/server/saved_objects/migrationsv2/types.ts
@@ -381,7 +381,7 @@ export interface LegacyDeleteState extends LegacyBaseState {
readonly controlState: 'LEGACY_DELETE';
}
-export type State =
+export type State = Readonly<
| FatalState
| InitState
| DoneState
@@ -411,7 +411,8 @@ export type State =
| LegacySetWriteBlockState
| LegacyReindexState
| LegacyReindexWaitForTaskState
- | LegacyDeleteState;
+ | LegacyDeleteState
+>;
export type AllControlStates = State['controlState'];
/**
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 972e220baae3e..3e6a69d159192 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2152,9 +2152,9 @@ export interface SavedObjectExportBaseOptions {
// @public
export interface SavedObjectMigrationContext {
- convertToMultiNamespaceTypeVersion?: string;
- log: SavedObjectsMigrationLogger;
- migrationVersion: string;
+ readonly convertToMultiNamespaceTypeVersion?: string;
+ readonly log: SavedObjectsMigrationLogger;
+ readonly migrationVersion: string;
}
// @public
diff --git a/src/plugins/dashboard/common/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts
index 16ab470ce7d6f..9f0858759d0d9 100644
--- a/src/plugins/dashboard/common/saved_dashboard_references.ts
+++ b/src/plugins/dashboard/common/saved_dashboard_references.ts
@@ -5,8 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
-import semverSatisfies from 'semver/functions/satisfies';
+import Semver from 'semver';
import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types';
import { DashboardContainerStateWithType, DashboardPanelState } from './types';
import { EmbeddablePersistableStateService } from '../../embeddable/common/types';
@@ -24,7 +23,7 @@ export interface SavedObjectAttributesAndReferences {
}
const isPre730Panel = (panel: Record): boolean => {
- return 'version' in panel ? semverSatisfies(panel.version, '<7.3') : true;
+ return 'version' in panel ? Semver.gt('7.3.0', panel.version) : true;
};
function dashboardAttributesToState(
From 507dd537881ec139fa7cfcf5e01e978a3ff99f85 Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 17 May 2021 14:02:00 +0200
Subject: [PATCH 11/11] fix stuff
---
.../definitions/calculations/counter_rate.tsx | 7 +-
.../calculations/cumulative_sum.tsx | 7 +-
.../definitions/calculations/differences.tsx | 7 +-
.../calculations/moving_average.tsx | 7 +-
.../operations/definitions/cardinality.tsx | 13 +-
.../operations/definitions/count.tsx | 13 +-
.../definitions/formula/editor/formula.scss | 9 ++
.../formula/editor/formula_editor.tsx | 20 ++-
.../formula/editor/formula_help.tsx | 132 +++++++++++------
.../operations/definitions/formula/util.ts | 140 +++++++++++++-----
.../operations/definitions/last_value.tsx | 7 +-
.../operations/definitions/metrics.tsx | 12 +-
.../operations/definitions/percentile.tsx | 7 +-
13 files changed, 269 insertions(+), 112 deletions(-)
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx
index 8f88de2de6cd7..5efb4dbb44767 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx
@@ -126,14 +126,17 @@ export const counterRateOperation: OperationDefinition<
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx
index 06807f61f7316..5c26f61bd6390 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx
@@ -119,13 +119,16 @@ export const cumulativeSumOperation: OperationDefinition<
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx
index dd4f0c8c76bba..63e1b4bff648e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx
@@ -116,13 +116,16 @@ export const derivativeOperation: OperationDefinition<
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx
index 2554a2bdd2c6c..afef871ee733a 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx
@@ -140,7 +140,7 @@ export const movingAverageOperation: OperationDefinition<
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx
index 16945bc185988..8d990c7740ec5 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx
@@ -117,12 +117,19 @@ export const cardinalityOperation: OperationDefinition
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx
index 0544bc4f9f460..ca1feed4c3af2 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx
@@ -97,12 +97,19 @@ export const countOperation: OperationDefinition 500")\`
+Example: Calculate the number of documents:
+\`\`\`
+count()
+\`\`\`
+
+Example: Calculate the number of documents matching a certain filter:
+\`\`\`
+count(kql="price > 500")
+\`\`\`
`,
})}
/>
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss
index 40cdbd58c3acf..5e97f592a0474 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss
@@ -30,6 +30,12 @@
padding: $euiSizeS;
}
+.lnsFormula__editorFooter {
+ // make sure docs are rendered in front of monaco
+ z-index: 1;
+ background-color: $euiColorLightestShade;
+}
+
.lnsFormula__editorHeaderGroup,
.lnsFormula__editorFooterGroup {
display: block; // Overrides EUI's styling of `display: flex` on `EuiFlexItem` components
@@ -65,6 +71,8 @@
.lnsFormula__docs--inline {
display: flex;
flex-direction: column;
+ // make sure docs are rendered in front of monaco
+ z-index: 1;
}
.lnsFormula__docsContent {
@@ -84,6 +92,7 @@
}
.lnsFormula__docsNav {
+ @include euiYScroll;
background: $euiColorLightestShade;
padding: $euiSizeS;
}
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 095e56e846005..0366aeab2043e 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
@@ -60,7 +60,7 @@ export function FormulaEditor({
}: ParamEditorProps) {
const [text, setText] = useState(currentColumn.params.formula);
const [warnings, setWarnings] = useState>([]);
- const [isHelpOpen, setIsHelpOpen] = useState(false);
+ const [isHelpOpen, setIsHelpOpen] = useState(isFullscreen);
const editorModel = React.useRef(
monaco.editor.createModel(text ?? '', LANGUAGE_ID)
);
@@ -566,11 +566,16 @@ export function FormulaEditor({
{isFullscreen ? (
- // TODO: Hook up the below `EuiLink` button so that it toggles the presence of the `.lnsFormula__docs--inline` element in fullscreen mode. Note that when docs are hidden, the `arrowDown` button should change to `arrowUp` and the label should change to `Show function reference`.
@@ -580,9 +585,10 @@ export function FormulaEditor({
})}
className="lnsFormula__editorHelp lnsFormula__editorHelp--inline"
color="text"
+ onClick={() => setIsHelpOpen(!isHelpOpen)}
>
-
+
) : (
@@ -650,7 +656,7 @@ export function FormulaEditor({
- {isFullscreen ? (
+ {isFullscreen && isHelpOpen ? (
+ {i18n.translate('xpack.lens.formulaDocumentation.mathSectionDescription', {
+ defaultMessage:
+ 'These functions will be executed for reach row of the resulting table using single values from the same row calculated using other functions.',
+ })}
+
+ ),
});
helpItems.push(
...getPossibleFunctions(indexPattern)
.filter((key) => key in tinymathFunctions)
+ .sort()
.map((key) => ({
label: `${key}`,
description: ,
- checked: selectedFunction === key ? ('on' as const) : undefined,
}))
);
@@ -63,6 +74,14 @@ function FormulaHelp({
defaultMessage: 'Elasticsearch',
}),
isGroupLabel: true,
+ description: (
+
+ {i18n.translate('xpack.lens.formulaDocumentation.elasticsearchSectionDescription', {
+ defaultMessage:
+ 'These functions will be executed on the raw documents for each row of the resulting table, aggregating all documents matching the break down dimensions into a single value.',
+ })}
+
+ ),
});
// Es aggs
@@ -73,13 +92,10 @@ function FormulaHelp({
key in operationDefinitionMap &&
operationDefinitionMap[key].documentation?.section === 'elasticsearch'
)
+ .sort()
.map((key) => ({
- label: `${key}: ${operationDefinitionMap[key].displayName}`,
+ label: key,
description: operationDefinitionMap[key].documentation?.description,
- checked:
- selectedFunction === `${key}: ${operationDefinitionMap[key].displayName}`
- ? ('on' as const)
- : undefined,
}))
);
@@ -88,6 +104,14 @@ function FormulaHelp({
defaultMessage: 'Column-wise calculation',
}),
isGroupLabel: true,
+ description: (
+
+ {i18n.translate('xpack.lens.formulaDocumentation.columnCalculationSectionDescription', {
+ defaultMessage:
+ 'These functions will be executed for reach row of the resulting table, using data from cells from other rows as well as the current value.',
+ })}
+
+ ),
});
// Calculations aggs
@@ -98,8 +122,9 @@ function FormulaHelp({
key in operationDefinitionMap &&
operationDefinitionMap[key].documentation?.section === 'calculation'
)
+ .sort()
.map((key) => ({
- label: `${key}: ${operationDefinitionMap[key].displayName}`,
+ label: key,
description: operationDefinitionMap[key].documentation?.description,
checked:
selectedFunction === `${key}: ${operationDefinitionMap[key].displayName}`
@@ -111,32 +136,42 @@ function FormulaHelp({
return (
<>
- Formula reference
+ {i18n.translate('xpack.lens.formulaDocumentation.header', {
+ defaultMessage: 'Formula reference',
+ })}
- {
- const chosenType = newOptions.find(({ checked }) => checked === 'on')!;
- if (!chosenType) {
- setSelectedFunction(undefined);
+
+ {helpItems.map((helpItem) => {
+ if (helpItem.isGroupLabel) {
+ return (
+ {
+ setSelectedFunction(helpItem.label);
+ }}
+ />
+ );
} else {
- setSelectedFunction(chosenType.label);
+ return (
+ {
+ setSelectedFunction(helpItem.label);
+ }}
+ />
+ );
}
- }}
- >
- {(list, search) => (
- <>
- {search}
- {list}
- >
- )}
-
+ })}
+
@@ -174,30 +209,37 @@ queries. If your search has a single quote in it, use a backslash to escape, lik
Math functions can take positional arguments, like pow(count(), 3) is the same as count() * count() * count()
-### Basic math
-
Use the symbols +, -, /, and * to perform basic math.
`,
description:
'Text is in markdown. Do not translate function names or field names like sum(bytes)',
})}
/>
+
{helpItems.map((item, index) => {
- if (item.isGroupLabel) {
- return null;
- } else {
- return (
- {
- if (el) {
- scrollTargets.current[item.label] = el;
- }
- }}
- >
- {item.description}
-
- );
- }
+ return (
+ {
+ if (el) {
+ scrollTargets.current[item.label] = el;
+ }
+ }}
+ >
+ {item.isGroupLabel ? (
+
+ {item.label}
+ {item.description}
+
+
+ ) : (
+
+ {item.description}
+ {helpItems.length - 1 !== index && }
+
+ )}
+
+ );
})}
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 6b083e5959378..2f68522b1ef51 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
@@ -85,13 +85,20 @@ export const tinymathFunctions: Record<
{ name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) },
],
help: `
-# add \`+\`
+### add(summand1: number, summand2: number) \`+\`
Adds up two numbers.
Also works with + symbol
-Example: Calculate the sum of two fields \`sum(price) + sum(tax)\`
+Example: Calculate the sum of two fields
+\`\`\`
+sum(price) + sum(tax)
+\`\`\`
-Example: Offset count by a static value \`add(count(), 5)\`
+Example: Offset count by a static value
+
+\`\`\`
+add(count(), 5)
+\`\`\`
`,
},
subtract: {
@@ -100,11 +107,14 @@ Example: Offset count by a static value \`add(count(), 5)\`
{ name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) },
],
help: `
-# subtract \`-\`
+### subtract(minuend: number, subtrahend: number) \`-\`
Subtracts the first number from the second number.
Also works with ${'`-`'} symbol
-Example: Calculate the range of a field ${'`subtract(max(bytes), min(bytes))`'}
+Example: Calculate the range of a field
+\`\`\`
+subtract(max(bytes), min(bytes))
+\`\`\`
`,
},
multiply: {
@@ -113,13 +123,19 @@ Example: Calculate the range of a field ${'`subtract(max(bytes), min(bytes))`'}
{ name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) },
],
help: `
-# multiply \`*\`
+### multiply(factor1: number, factor2: number) \`*\`
Multiplies two numbers.
Also works with ${'`*`'} symbol.
-Example: Calculate price after current tax rate ${'`sum(bytes) * last_value(tax_rate)`'}
+Example: Calculate price after current tax rate
+\`\`\`
+sum(bytes) * last_value(tax_rate)
+\`\`\`
-Example: Calculate price after constant tax rate \`multiply(sum(price), 1.2)\`
+Example: Calculate price after constant tax rate
+\`\`\`
+multiply(sum(price), 1.2)
+\`\`\`
`,
},
divide: {
@@ -128,11 +144,14 @@ Example: Calculate price after constant tax rate \`multiply(sum(price), 1.2)\`
{ name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) },
],
help: `
-# divide \`/\`
+### divide(dividend: number, divisor: number) \`/\`
Divides the first number by the second number.
Also works with ${'`/`'} symbol
-Example: Calculate profit margin \`sum(profit) / sum(revenue)\`
+Example: Calculate profit margin
+\`\`\`
+sum(profit) / sum(revenue)
+\`\`\`
`,
},
abs: {
@@ -140,7 +159,7 @@ Example: Calculate profit margin \`sum(profit) / sum(revenue)\`
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-# abs
+### abs(value: number)
Calculates absolute value. A negative value is multiplied by -1, a positive value stays the same.
Example: Calculate average distance to sea level ${'`abs(average(altitude))`'}
@@ -151,10 +170,13 @@ Example: Calculate average distance to sea level ${'`abs(average(altitude))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-# cbrt
+### cbrt(value: number)
Cube root of value.
-Example: Calculate side length from volume ${'`cbrt(last_value(volume))`'}
+Example: Calculate side length from volume
+\`\`\`
+cbrt(last_value(volume))
+\`\`\`
`,
},
ceil: {
@@ -162,10 +184,13 @@ Example: Calculate side length from volume ${'`cbrt(last_value(volume))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-# ceil
+### ceil(value: number)
Ceiling of value, rounds up.
-Example: Round up price to the next dollar ${'`ceil(sum(price))`'}
+Example: Round up price to the next dollar
+\`\`\`
+ceil(sum(price))
+\`\`\`
`,
},
clamp: {
@@ -175,21 +200,31 @@ Example: Round up price to the next dollar ${'`ceil(sum(price))`'}
{ name: i18n.translate('xpack.lens.formula.max', { defaultMessage: 'max' }) },
],
help: `
-# clamp
+### clamp(value: number, minimum: number, maximum: number)
Limits the value from a minimum to maximum.
-Example: Make sure to catch outliers ${'`clamp(average(bytes), percentile(bytes, percentile=5), percentile(bytes, percentile=95))`'}
- `,
+Example: Make sure to catch outliers
+\`\`\`
+clamp(
+ average(bytes),
+ percentile(bytes, percentile=5),
+ percentile(bytes, percentile=95)
+)
+\`\`\`
+`,
},
cube: {
positionalArguments: [
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-# cube
+### cube(value: number)
Calculates the cube of a number.
-Example: Calculate volume from side length ${'`cube(last_value(length))`'}
+Example: Calculate volume from side length
+\`\`\`
+cube(last_value(length))
+\`\`\`
`,
},
exp: {
@@ -197,10 +232,13 @@ Example: Calculate volume from side length ${'`cube(last_value(length))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-# exp
+### exp(value: number)
Raises e to the nth power.
-Example: Calculate the natural expontential function ${'`exp(last_value(duration))`'}
+Example: Calculate the natural expontential function
+\`\`\`
+exp(last_value(duration))
+\`\`\`
`,
},
fix: {
@@ -208,10 +246,13 @@ Example: Calculate the natural expontential function ${'`exp(last_value(duration
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-# fix
+### fix(value: number)
For positive values, takes the floor. For negative values, takes the ceiling.
-Example: Rounding towards zero ${'`fix(sum(profit))`'}
+Example: Rounding towards zero
+\`\`\`
+fix(sum(profit))
+\`\`\`
`,
},
floor: {
@@ -219,10 +260,13 @@ Example: Rounding towards zero ${'`fix(sum(profit))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-# floor
+### floor(value: number)
Round down to nearest integer value
-Example: Round down a price ${'`floor(sum(price))`'}
+Example: Round down a price
+\`\`\`
+floor(sum(price))
+\`\`\`
`,
},
log: {
@@ -234,10 +278,13 @@ Example: Round down a price ${'`floor(sum(price))`'}
},
],
help: `
-# log
+### log(value: number, base?: number)
Logarithm with optional base. The natural base e is used as default.
-Example: Calculate number of bits required to store values ${'`log(max(price), 2)`'}
+Example: Calculate number of bits required to store values
+\`\`\`
+log(max(price), 2)
+\`\`\`
`,
},
// TODO: check if this is valid for Tinymath
@@ -259,24 +306,30 @@ Example: Calculate number of bits required to store values ${'`log(max(price), 2
},
],
help: `
-# mod
+### mod(value: number)
Remainder after dividing the function by a number
-Example: Calculate last three digits of a value ${'`mod(sum(price), 1000)`'}
+Example: Calculate last three digits of a value
+\`\`\`
+mod(sum(price), 1000)
+\`\`\`
`,
},
pow: {
positionalArguments: [
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
{
- name: i18n.translate('xpack.lens.formula.base', { defaultMessage: 'base' }),
+ name: i18n.translate('xpack.lens.\\formula.base', { defaultMessage: 'base' }),
},
],
help: `
-# pow
+### pow(value: number, power: number)
Raises the value to a certain power. The second argument is required
-Example: Calculate volume based on side length ${'`pow(last_value(length), 3)`'}
+Example: Calculate volume based on side length
+\`\`\`
+pow(last_value(length), 3)
+\`\`\`
`,
},
round: {
@@ -288,10 +341,13 @@ Example: Calculate volume based on side length ${'`pow(last_value(length), 3)`'}
},
],
help: `
-# round
+### round(value: number, digits: number = 0)
Rounds to a specific number of decimal places, default of 0
-Example: Round to the cent ${'`round(sum(price), 2)`'}
+Example: Round to the cent
+\`\`\`
+round(sum(price), 2)
+\`\`\`
`,
},
sqrt: {
@@ -299,10 +355,13 @@ Example: Round to the cent ${'`round(sum(price), 2)`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-# sqrt
+### sqrt(value: number)
Square root of a positive value only
-Example: Calculate side length based on area ${'`sqrt(last_value(area))`'}
+Example: Calculate side length based on area
+\`\`\`
+sqrt(last_value(area))
+\`\`\`
`,
},
square: {
@@ -310,10 +369,13 @@ Example: Calculate side length based on area ${'`sqrt(last_value(area))`'}
{ name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) },
],
help: `
-# square
+### square(value: number)
Raise the value to the 2nd power
-Example: Calculate area based on side length ${'`square(last_value(length))`'}
+Example: Calculate area based on side length
+\`\`\`
+square(last_value(length))
+\`\`\`
`,
},
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx
index 8df92254b505f..5ec17b6ac3e6f 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx
@@ -272,13 +272,16 @@ export const lastValueOperation: OperationDefinition
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx
index 6a3856feb0d2b..2c5bda1d2870d 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx
@@ -133,13 +133,19 @@ function buildMetricOperation>({