diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
index 6c2c01d944cd9..c9761adf3af67 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
@@ -161,7 +161,7 @@ export function WorkspacePanel({
const expression = useMemo(
() => {
- if (!configurationValidationError || configurationValidationError.length === 0) {
+ if (!configurationValidationError?.length) {
try {
return buildExpression({
visualization: activeVisualization,
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 4c3def0e5bc7f..de4aa83e203cc 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
@@ -28,6 +28,7 @@ import {
updateColumnParam,
resetIncomplete,
FieldBasedIndexPatternColumn,
+ OperationType,
} from '../operations';
import { mergeLayer } from '../state_helpers';
import { FieldSelect } from './field_select';
@@ -36,6 +37,7 @@ import { BucketNestingEditor } from './bucket_nesting_editor';
import { IndexPattern, IndexPatternLayer } from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
import { FormatSelector } from './format_selector';
+import { ReferenceEditor } from './reference_editor';
import { TimeScaling } from './time_scaling';
const operationPanels = getOperationDisplay();
@@ -183,9 +185,12 @@ export function DimensionEditor(props: DimensionEditorProps) {
compatibleWithCurrentField ? '' : ' incompatible'
}`,
onClick() {
- if (operationDefinitionMap[operationType].input === 'none') {
+ if (
+ operationDefinitionMap[operationType].input === 'none' ||
+ operationDefinitionMap[operationType].input === 'fullReference'
+ ) {
+ // Clear invalid state because we are reseting to a valid column
if (selectedColumn?.operationType === operationType) {
- // Clear invalid state because we are reseting to a valid column
if (incompleteInfo) {
setState(
mergeLayer({
@@ -293,6 +298,34 @@ export function DimensionEditor(props: DimensionEditorProps) {
+ {!incompleteInfo &&
+ selectedColumn &&
+ 'references' in selectedColumn &&
+ selectedOperationDefinition?.input === 'fullReference' ? (
+ <>
+ {selectedColumn.references.map((referenceId, index) => {
+ const validation = selectedOperationDefinition.requiredReferences[index];
+
+ return (
+
{
+ setState(mergeLayer({ state, layerId, newLayer }));
+ }}
+ validation={validation}
+ currentIndexPattern={currentIndexPattern}
+ existingFields={state.existingFields}
+ selectionStyle={selectedOperationDefinition.selectionStyle}
+ />
+ );
+ })}
+
+ >
+ ) : null}
+
{!selectedColumn ||
selectedOperationDefinition?.input === 'field' ||
(incompleteOperation && operationDefinitionMap[incompleteOperation].input === 'field') ? (
@@ -447,11 +480,11 @@ export function DimensionEditor(props: DimensionEditorProps) {
}
function getErrorMessage(
selectedColumn: IndexPatternColumn | undefined,
- incompatibleSelectedOperationType: boolean,
+ incompleteOperation: boolean,
input: 'none' | 'field' | 'fullReference' | undefined,
fieldInvalid: boolean
) {
- if (selectedColumn && incompatibleSelectedOperationType) {
+ if (selectedColumn && incompleteOperation) {
if (input === 'field') {
return i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', {
defaultMessage: 'To use this function, select a different field.',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
index d4197f9395660..58c06070f288e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
@@ -1068,6 +1068,10 @@ describe('IndexPatternDimensionEditorPanel', () => {
});
it('should support selecting the operation before the field', () => {
+ setState.mockImplementation((newState) => {
+ wrapper.setProps({ state: newState });
+ });
+
wrapper = mount();
wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click');
@@ -1100,15 +1104,15 @@ describe('IndexPatternDimensionEditorPanel', () => {
layers: {
first: {
...state.layers.first,
+ columnOrder: ['col1', 'col2'],
columns: {
...state.layers.first.columns,
col2: expect.objectContaining({
- sourceField: 'bytes',
operationType: 'avg',
- // Other parts of this don't matter for this test
+ sourceField: 'bytes',
}),
},
- columnOrder: ['col1', 'col2'],
+ incompleteColumns: {},
},
},
});
@@ -1178,9 +1182,15 @@ describe('IndexPatternDimensionEditorPanel', () => {
});
it('should indicate compatible fields when selecting the operation first', () => {
+ setState.mockImplementation((newState) => {
+ wrapper.setProps({ state: newState });
+ });
+
wrapper = mount();
- wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click');
+ act(() => {
+ wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click');
+ });
const options = wrapper
.find(EuiComboBox)
@@ -1242,9 +1252,13 @@ describe('IndexPatternDimensionEditorPanel', () => {
expect(items.map(({ label }: { label: React.ReactNode }) => label)).toEqual([
'Average',
'Count',
+ 'Counter rate',
+ 'Cumulative sum',
+ 'Differences',
'Maximum',
'Median',
'Minimum',
+ 'Moving average',
'Sum',
'Unique count',
'\u00a0',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts
index 817fdf637f001..9d55a9d5f7522 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts
@@ -49,7 +49,7 @@ export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix
supportedFieldsByOperation[operation.operationType] = new Set();
}
supportedFieldsByOperation[operation.operationType]?.add(operation.field);
- } else if (operation.type === 'none') {
+ } else {
supportedOperationsWithoutField.add(operation.operationType);
}
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx
new file mode 100644
index 0000000000000..a49772a18e09e
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx
@@ -0,0 +1,319 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import './dimension_editor.scss';
+import _ from 'lodash';
+import React, { useMemo } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiFormRow, EuiSpacer, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
+import { OperationSupportMatrix } from './operation_support';
+import type { OperationType } from '../indexpattern';
+import {
+ operationDefinitionMap,
+ getOperationDisplay,
+ insertOrReplaceColumn,
+ replaceColumn,
+ deleteColumn,
+ RequiredReference,
+ isOperationAllowedAsReference,
+ FieldBasedIndexPatternColumn,
+} from '../operations';
+import { FieldSelect } from './field_select';
+import { hasField } from '../utils';
+import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types';
+import { trackUiEvent } from '../../lens_ui_telemetry';
+
+const operationPanels = getOperationDisplay();
+
+export interface ReferenceEditorProps {
+ layer: IndexPatternLayer;
+ parentColumnId: string;
+ selectionStyle: 'full' | 'field';
+ validation: RequiredReference;
+ columnId: string;
+ updateLayer: (newLayer: IndexPatternLayer) => void;
+ currentIndexPattern: IndexPattern;
+ existingFields: IndexPatternPrivateState['existingFields'];
+}
+
+export function ReferenceEditor(props: ReferenceEditorProps) {
+ const {
+ layer,
+ columnId,
+ updateLayer,
+ currentIndexPattern,
+ existingFields,
+ validation,
+ selectionStyle,
+ } = props;
+
+ const column = layer.columns[columnId];
+ const selectedOperationDefinition = column && operationDefinitionMap[column.operationType];
+
+ const incompleteInfo = layer.incompleteColumns ? layer.incompleteColumns[columnId] : undefined;
+ const incompleteOperation = incompleteInfo?.operationType;
+ const incompleteField = incompleteInfo?.sourceField ?? null;
+
+ // Basically the operation support matrix, but different validation
+ const operationSupportMatrix: OperationSupportMatrix & {
+ operationTypes: Set;
+ } = useMemo(() => {
+ const operationTypes: Set = new Set();
+ const operationWithoutField: Set = new Set();
+ const operationByField: Partial>> = {};
+ const fieldByOperation: Partial>> = {};
+ Object.values(operationDefinitionMap)
+ .sort((op1, op2) => {
+ return op1.displayName.localeCompare(op2.displayName);
+ })
+ .forEach((op) => {
+ if (op.input === 'field') {
+ const allFields = currentIndexPattern.fields.filter((field) =>
+ isOperationAllowedAsReference({ operationType: op.type, validation, field })
+ );
+ if (allFields.length) {
+ operationTypes.add(op.type);
+ fieldByOperation[op.type] = new Set(allFields.map(({ name }) => name));
+ allFields.forEach((field) => {
+ if (!operationByField[field.name]) {
+ operationByField[field.name] = new Set();
+ }
+ operationByField[field.name]?.add(op.type);
+ });
+ }
+ } else if (isOperationAllowedAsReference({ operationType: op.type, validation })) {
+ operationTypes.add(op.type);
+ operationWithoutField.add(op.type);
+ }
+ });
+ return {
+ operationTypes,
+ operationWithoutField,
+ operationByField,
+ fieldByOperation,
+ };
+ }, [currentIndexPattern, validation]);
+
+ const functionOptions: Array> = [];
+ operationSupportMatrix.operationTypes.forEach((operationType) => {
+ const label = operationPanels[operationType].displayName;
+ // const isCompatible = !column || column && hasField(column) &&
+
+ functionOptions.push({
+ label,
+ value: operationType,
+ className: 'lnsIndexPatternDimensionEditor__operation',
+ // 'data-test-subj': `lns-indexPatternDimension-${operationType}${
+ // compatibleWithCurrentField ? '' : ' incompatible'
+ // }`,
+ });
+ });
+
+ function onChooseFunction(operationType: OperationType) {
+ // Clear invalid state because we are creating a valid column
+ if (column?.operationType === operationType) {
+ return;
+ }
+ const possibleFieldNames = operationSupportMatrix.fieldByOperation[operationType];
+ const possibleField =
+ possibleFieldNames?.size === 1
+ ? currentIndexPattern.getFieldByName(possibleFieldNames.values().next().value)
+ : undefined;
+
+ updateLayer(
+ insertOrReplaceColumn({
+ layer,
+ columnId,
+ op: operationType,
+ indexPattern: currentIndexPattern,
+ field: possibleField,
+ })
+ );
+ trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
+ return;
+ }
+
+ const selectedOption = incompleteInfo?.operationType
+ ? [functionOptions.find(({ value }) => value === incompleteInfo.operationType)!]
+ : column
+ ? [functionOptions.find(({ value }) => value === column.operationType)!]
+ : [];
+
+ // The current operation is invalid if
+ // const invalidOperation =
+ // Boolean(incompleteInfo?.operationType) ||
+ // (column &&
+ // selectedOperationDefinition.input === 'field' &&
+ // !operationSupportMatrix.fieldByOperation[column.operationType]?.size);
+ const invalidField =
+ column &&
+ selectedOperationDefinition.input === 'field' &&
+ !operationSupportMatrix.fieldByOperation[column.operationType]?.size;
+
+ return (
+
+
+ {selectionStyle !== 'field' ? (
+ <>
+
+ {
+ if (choices.length === 0) {
+ // onDeleteColumn();
+ updateLayer(deleteColumn({ layer, columnId }));
+ return;
+ }
+
+ trackUiEvent('indexpattern_dimension_field_changed');
+
+ onChooseFunction(choices[0].value!);
+ }}
+ />
+
+
+ >
+ ) : null}
+
+ {!column ||
+ // (incompatibleSelectedOperationType &&
+ // operationDefinitionMap[incompatibleSelectedOperationType].input === 'field') ? (
+ selectedOperationDefinition.input === 'field' ? (
+
+ {
+ updateLayer(deleteColumn({ layer, columnId }));
+ }}
+ onChoose={(choice) => {
+ let newLayer: IndexPatternLayer;
+ if (
+ !incompleteInfo?.operationType &&
+ column &&
+ 'field' in choice &&
+ choice.operationType === column.operationType
+ ) {
+ // Replaces just the field
+ newLayer = replaceColumn({
+ layer,
+ columnId,
+ indexPattern: currentIndexPattern,
+ op: choice.operationType,
+ field: currentIndexPattern.getFieldByName(choice.field)!,
+ });
+ } else {
+ // Finds a new operation
+ const compatibleOperations =
+ ('field' in choice && operationSupportMatrix.operationByField[choice.field]) ||
+ new Set();
+ let operation;
+ if (compatibleOperations.size > 0) {
+ operation =
+ incompleteInfo?.operationType &&
+ compatibleOperations.has(incompleteInfo.operationType as OperationType)
+ ? incompleteInfo.operationType
+ : compatibleOperations.values().next().value;
+ } else if ('field' in choice) {
+ operation = choice.operationType;
+ }
+ newLayer = insertOrReplaceColumn({
+ layer,
+ columnId,
+ field: currentIndexPattern.getFieldByName(choice.field),
+ indexPattern: currentIndexPattern,
+ op: operation as OperationType,
+ });
+ }
+ updateLayer(newLayer);
+ // setInvalidOperationType(null);
+ }}
+ />
+
+ ) : null}
+
+
+ );
+}
+
+{
+ /* function getErrorMessage(
+ column: IndexPatternColumn | undefined,
+ incompatibleSelectedOperationType: boolean,
+ input: 'none' | 'field' | 'fullReference' | undefined,
+ fieldInvalid: boolean
+) {
+ if (column && incompatibleSelectedOperationType) {
+ if (input === 'field') {
+ return i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', {
+ defaultMessage: 'To use this function, select a different field.',
+ });
+ }
+ return i18n.translate('xpack.lens.indexPattern.chooseFieldLabel', {
+ defaultMessage: 'To use this function, select a field.',
+ });
+ }
+ if (fieldInvalid) {
+ return i18n.translate('xpack.lens.indexPattern.invalidFieldLabel', {
+ defaultMessage: 'Invalid field. Check your index pattern or pick another field.',
+ });
+ }
+} */
+}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
index 3c1c8d5f2c006..eea17613cd249 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
@@ -982,8 +982,8 @@ describe('IndexPattern Data Source', () => {
currentIndexPatternId: '1',
};
expect(indexPatternDatasource.getErrorMessages(state)).toEqual([
- { shortMessage: 'error 1', longMessage: '' },
- { shortMessage: 'error 2', longMessage: '' },
+ { longMessage: 'error 1', shortMessage: '' },
+ { longMessage: 'error 2', shortMessage: '' },
]);
expect(getErrorMessages).toHaveBeenCalledTimes(1);
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
index 948619c6b07e5..badbc7bf5cdf3 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
@@ -370,8 +370,8 @@ export function getIndexPatternDatasource({
const layerErrors = Object.values(state.layers).flatMap((layer) =>
(getErrorMessages(layer) ?? []).map((message) => ({
- shortMessage: message,
- longMessage: '',
+ shortMessage: '', // Not displayed currently
+ longMessage: message,
}))
);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
index 9fbad553d441a..e393dd8ead63f 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
@@ -1940,6 +1940,12 @@ describe('IndexPattern Data Source suggestions', () => {
const suggestions = getDatasourceSuggestionsFromCurrentState(state);
expect(suggestions).toEqual([]);
});
+
+ describe('references', () => {
+ it('does not simplify a reference based operation to an invalid state', () => {
+ throw new Error('todo: write all the tests for references');
+ });
+ });
});
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts
index 263b4646c9feb..4afaeaa8ee38f 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts
@@ -17,6 +17,7 @@ import {
operationDefinitionMap,
IndexPatternColumn,
OperationType,
+ getExistingColumnGroups,
} from './operations';
import { hasField, hasInvalidFields } from './utils';
import {
@@ -221,7 +222,7 @@ function getExistingLayerSuggestionsForField(
);
}
- const [, metrics] = separateBucketColumns(layer);
+ const [, metrics, references] = getExistingColumnGroups(layer);
if (metrics.length === 1) {
const layerWithReplacedMetric = replaceColumn({
layer,
@@ -377,7 +378,7 @@ export function getDatasourceSuggestionsFromCurrentState(
.filter(([_id, layer]) => layer.columnOrder.length && layer.indexPatternId)
.map(([layerId, layer]) => {
const indexPattern = state.indexPatterns[layer.indexPatternId];
- const [buckets, metrics] = separateBucketColumns(layer);
+ const [buckets, metrics, references] = getExistingColumnGroups(layer);
const timeDimension = layer.columnOrder.find(
(columnId) =>
layer.columns[columnId].isBucketed && layer.columns[columnId].dataType === 'date'
@@ -570,7 +571,11 @@ function createSuggestionWithDefaultDateHistogram(
function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layerId: string) {
const layer = state.layers[layerId];
- const [availableBucketedColumns, availableMetricColumns] = separateBucketColumns(layer);
+ const [
+ availableBucketedColumns,
+ availableMetricColumns,
+ availableReferenceColumns,
+ ] = getExistingColumnGroups(layer);
return _.flatten(
availableBucketedColumns.map((_col, index) => {
@@ -623,7 +628,3 @@ function getMetricSuggestionTitle(layer: IndexPatternLayer, onlyMetric: boolean)
'Title of a suggested chart containing only a single numerical metric calculated over all available data',
});
}
-
-function separateBucketColumns(layer: IndexPatternLayer) {
- return partition(layer.columnOrder, (columnId) => layer.columns[columnId].isBucketed);
-}
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 0cfba4cfc739f..66e09712c564c 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
@@ -60,7 +60,8 @@ export const counterRateOperation: OperationDefinition<
};
},
getDefaultLabel: (column, indexPattern, columns) => {
- return ofName(columns[column.references[0]]?.label, column.timeScale);
+ const ref = columns[column.references[0]];
+ return ofName(ref && 'sourceField' in ref ? ref.sourceField : undefined, column.timeScale);
},
toExpression: (layer, columnId) => {
return dateBasedOperationToExpression(layer, columnId, 'lens_counter_rate');
@@ -69,7 +70,7 @@ export const counterRateOperation: OperationDefinition<
const metric = layer.columns[referenceIds[0]];
const timeScale = previousColumn?.timeScale || DEFAULT_TIME_SCALE;
return {
- label: ofName(metric?.label, timeScale),
+ label: ofName(metric && 'sourceField' in metric ? metric.sourceField : undefined, timeScale),
dataType: 'number',
operationType: 'counter_rate',
isBucketed: false,
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 9244aaaf90ab7..b0d99934cba74 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
@@ -12,7 +12,7 @@ import { OperationDefinition } from '..';
const ofName = (name?: string) => {
return i18n.translate('xpack.lens.indexPattern.cumulativeSumOf', {
- defaultMessage: 'Cumulative sum rate of {name}',
+ defaultMessage: 'Cumulative sum of {name}',
values: {
name:
name ??
@@ -54,15 +54,16 @@ export const cumulativeSumOperation: OperationDefinition<
};
},
getDefaultLabel: (column, indexPattern, columns) => {
- return ofName(columns[column.references[0]]?.label);
+ const ref = columns[column.references[0]];
+ return ofName(ref && 'sourceField' in ref ? ref.sourceField : undefined);
},
toExpression: (layer, columnId) => {
return dateBasedOperationToExpression(layer, columnId, 'cumulative_sum');
},
buildColumn: ({ referenceIds, previousColumn, layer }) => {
- const metric = layer.columns[referenceIds[0]];
+ const ref = layer.columns[referenceIds[0]];
return {
- label: ofName(metric?.label),
+ label: ofName(ref && 'sourceField' in ref ? ref.sourceField : undefined),
dataType: 'number',
operationType: 'cumulative_sum',
isBucketed: false,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/derivative.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/derivative.tsx
index 41fe361c7ba9c..0b7f0a79f6d11 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/derivative.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/derivative.tsx
@@ -59,15 +59,19 @@ export const derivativeOperation: OperationDefinition<
};
},
getDefaultLabel: (column, indexPattern, columns) => {
- return ofName(columns[column.references[0]]?.label, column.timeScale);
+ const ref = columns[column.references[0]];
+ return ofName(ref && 'sourceField' in ref ? ref.sourceField : undefined, column.timeScale);
},
toExpression: (layer, columnId) => {
return dateBasedOperationToExpression(layer, columnId, 'derivative');
},
buildColumn: ({ referenceIds, previousColumn, layer }) => {
- const metric = layer.columns[referenceIds[0]];
+ const ref = layer.columns[referenceIds[0]];
return {
- label: ofName(metric?.label, previousColumn?.timeScale),
+ label: ofName(
+ ref && 'sourceField' in ref ? ref.sourceField : undefined,
+ previousColumn?.timeScale
+ ),
dataType: 'number',
operationType: 'derivative',
isBucketed: false,
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 522899662fbd1..1c9bb5533c7b7 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
@@ -50,7 +50,7 @@ export const movingAverageOperation: OperationDefinition<
type: 'moving_average',
priority: 1,
displayName: i18n.translate('xpack.lens.indexPattern.movingAverage', {
- defaultMessage: 'Moving Average',
+ defaultMessage: 'Moving average',
}),
input: 'fullReference',
selectionStyle: 'full',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
index 7ffbeac39c6f5..9acc033e50756 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
@@ -190,6 +190,44 @@ describe('state_helpers', () => {
).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2'] }));
});
+ it('should insert a metric after buckets, but before references', () => {
+ const layer: IndexPatternLayer = {
+ indexPatternId: '1',
+ columnOrder: ['col1'],
+ columns: {
+ col1: {
+ label: 'Date histogram of timestamp',
+ dataType: 'date',
+ isBucketed: true,
+
+ // Private
+ operationType: 'date_histogram',
+ sourceField: 'timestamp',
+ params: {
+ interval: 'h',
+ },
+ },
+ col3: {
+ label: 'Reference',
+ dataType: 'number',
+ isBucketed: false,
+
+ operationType: 'cumulative_sum',
+ references: ['col2'],
+ },
+ },
+ };
+ expect(
+ insertNewColumn({
+ layer,
+ indexPattern,
+ columnId: 'col2',
+ op: 'count',
+ field: documentField,
+ })
+ ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] }));
+ });
+
it('should insert new buckets at the end of previous buckets', () => {
const layer: IndexPatternLayer = {
indexPatternId: '1',
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 b16418d44ba33..b45a0bd73818f 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
@@ -67,9 +67,15 @@ export function insertNewColumn({
const possibleOperation = operationDefinition.getPossibleOperation();
const isBucketed = Boolean(possibleOperation.isBucketed);
if (isBucketed) {
- return addBucket(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId);
+ return updateDefaultLabels(
+ addBucket(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId),
+ indexPattern
+ );
} else {
- return addMetric(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId);
+ return updateDefaultLabels(
+ addMetric(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId),
+ indexPattern
+ );
}
}
@@ -131,7 +137,7 @@ export function insertNewColumn({
const possibleOperation = operationDefinition.getPossibleOperation();
const isBucketed = Boolean(possibleOperation.isBucketed);
if (isBucketed) {
- return addBucket(
+ tempLayer = addBucket(
tempLayer,
operationDefinition.buildColumn({
...baseOptions,
@@ -141,7 +147,7 @@ export function insertNewColumn({
columnId
);
} else {
- return addMetric(
+ tempLayer = addMetric(
tempLayer,
operationDefinition.buildColumn({
...baseOptions,
@@ -151,6 +157,8 @@ export function insertNewColumn({
columnId
);
}
+
+ return updateDefaultLabels(tempLayer, indexPattern);
}
const invalidFieldName = (layer.incompleteColumns ?? {})[columnId]?.sourceField;
@@ -165,16 +173,22 @@ export function insertNewColumn({
}
const isBucketed = Boolean(possibleOperation.isBucketed);
if (isBucketed) {
- return addBucket(
- layer,
- operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }),
- columnId
+ return updateDefaultLabels(
+ addBucket(
+ layer,
+ operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }),
+ columnId
+ ),
+ indexPattern
);
} else {
- return addMetric(
- layer,
- operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }),
- columnId
+ return updateDefaultLabels(
+ addMetric(
+ layer,
+ operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }),
+ columnId
+ ),
+ indexPattern
);
}
} else if (!field) {
@@ -200,16 +214,14 @@ export function insertNewColumn({
}
const isBucketed = Boolean(possibleOperation.isBucketed);
if (isBucketed) {
- return addBucket(
- layer,
- operationDefinition.buildColumn({ ...baseOptions, layer, field }),
- columnId
+ return updateDefaultLabels(
+ addBucket(layer, operationDefinition.buildColumn({ ...baseOptions, layer, field }), columnId),
+ indexPattern
);
} else {
- return addMetric(
- layer,
- operationDefinition.buildColumn({ ...baseOptions, layer, field }),
- columnId
+ return updateDefaultLabels(
+ addMetric(layer, operationDefinition.buildColumn({ ...baseOptions, layer, field }), columnId),
+ indexPattern
);
}
}
@@ -251,6 +263,8 @@ export function replaceColumn({
});
}
+ tempLayer = resetIncomplete(tempLayer, columnId);
+
if (operationDefinition.input === 'fullReference') {
const referenceIds = operationDefinition.requiredReferences.map(() => generateId());
@@ -263,11 +277,14 @@ export function replaceColumn({
previousColumn,
}),
};
- return {
- ...tempLayer,
- columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }),
- columns: newColumns,
- };
+ return updateDefaultLabels(
+ {
+ ...tempLayer,
+ columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }),
+ columns: newColumns,
+ },
+ indexPattern
+ );
}
if (operationDefinition.input === 'none') {
@@ -275,11 +292,14 @@ export function replaceColumn({
newColumn = adjustLabel(newColumn, previousColumn);
const newColumns = { ...tempLayer.columns, [columnId]: newColumn };
- return {
- ...tempLayer,
- columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }),
- columns: adjustColumnReferencesForChangedColumn(newColumns, columnId),
- };
+ return updateDefaultLabels(
+ {
+ ...tempLayer,
+ columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }),
+ columns: adjustColumnReferencesForChangedColumn(newColumns, columnId),
+ },
+ indexPattern
+ );
}
if (!field) {
@@ -296,11 +316,14 @@ export function replaceColumn({
newColumn = adjustLabel(newColumn, previousColumn);
const newColumns = { ...tempLayer.columns, [columnId]: newColumn };
- return {
- ...tempLayer,
- columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }),
- columns: adjustColumnReferencesForChangedColumn(newColumns, columnId),
- };
+ return updateDefaultLabels(
+ {
+ ...tempLayer,
+ columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }),
+ columns: adjustColumnReferencesForChangedColumn(newColumns, columnId),
+ },
+ indexPattern
+ );
} else if (
operationDefinition.input === 'field' &&
field &&
@@ -310,12 +333,20 @@ export function replaceColumn({
// Same operation, new field
const newColumn = operationDefinition.onFieldChange(previousColumn, field);
- const newColumns = { ...layer.columns, [columnId]: adjustLabel(newColumn, previousColumn) };
- return {
- ...layer,
- columnOrder: getColumnOrder({ ...layer, columns: newColumns }),
- columns: adjustColumnReferencesForChangedColumn(newColumns, columnId),
- };
+ if (previousColumn.customLabel) {
+ newColumn.customLabel = true;
+ newColumn.label = previousColumn.label;
+ }
+
+ const newColumns = { ...layer.columns, [columnId]: newColumn };
+ return updateDefaultLabels(
+ {
+ ...resetIncomplete(layer, columnId),
+ columnOrder: getColumnOrder({ ...layer, columns: newColumns }),
+ columns: adjustColumnReferencesForChangedColumn(newColumns, columnId),
+ },
+ indexPattern
+ );
} else {
throw new Error('nothing changed');
}
@@ -376,7 +407,6 @@ function addMetric(
...layer.columns,
[addedColumnId]: column,
},
- columnOrder: [...layer.columnOrder, addedColumnId],
};
return { ...tempLayer, columnOrder: getColumnOrder(tempLayer) };
}
@@ -478,6 +508,8 @@ export function deleteColumn({
const newIncomplete = { ...(newLayer.incompleteColumns || {}) };
delete newIncomplete[columnId];
+ // TODO: Deleting should also update labels, but it's not actually required for now because of the
+ // reference UI not supporting inner deletion
return { ...newLayer, columnOrder: getColumnOrder(newLayer), incompleteColumns: newIncomplete };
}
@@ -499,7 +531,7 @@ export function getColumnOrder(layer: IndexPatternLayer): string[] {
const [direct, referenceBased] = _.partition(
entries,
- ([id, col]) => operationDefinitionMap[col.operationType].input !== 'fullReference'
+ ([, col]) => operationDefinitionMap[col.operationType].input !== 'fullReference'
);
// If a reference has another reference as input, put it last in sort order
referenceBased.sort(([idA, a], [idB, b]) => {
@@ -520,7 +552,7 @@ export function getColumnOrder(layer: IndexPatternLayer): string[] {
}
// Splits existing columnOrder into the three categories
-function getExistingColumnGroups(layer: IndexPatternLayer): [string[], string[], string[]] {
+export function getExistingColumnGroups(layer: IndexPatternLayer): [string[], string[], string[]] {
const [direct, referenced] = partition(
layer.columnOrder,
(columnId) => layer.columns[columnId] && !('references' in layer.columns[columnId])
@@ -579,7 +611,7 @@ export function getErrorMessages(layer: IndexPatternLayer): string[] | undefined
if (!layer.columns[referenceId]) {
errors.push(
i18n.translate('xpack.lens.indexPattern.missingReferenceError', {
- defaultMessage: 'Dimension {dimensionLabel} is incomplete',
+ defaultMessage: '"{dimensionLabel}" is missing required configuration',
values: {
dimensionLabel: column.label,
},
@@ -620,7 +652,7 @@ export function isReferenced(layer: IndexPatternLayer, columnId: string): boolea
return allReferences.includes(columnId);
}
-function isColumnValidAsReference({
+export function isColumnValidAsReference({
column,
validation,
}: {
@@ -637,7 +669,7 @@ function isColumnValidAsReference({
);
}
-function isOperationAllowedAsReference({
+export function isOperationAllowedAsReference({
operationType,
validation,
field,
@@ -665,6 +697,29 @@ function isOperationAllowedAsReference({
);
}
+// Labels need to be updated when columns are added because reference-based column labels
+// are sometimes copied into the parents
+function updateDefaultLabels(
+ layer: IndexPatternLayer,
+ indexPattern: IndexPattern
+): IndexPatternLayer {
+ const copiedColumns = { ...layer.columns };
+ layer.columnOrder.forEach((id) => {
+ const col = copiedColumns[id];
+ if (!col.customLabel) {
+ copiedColumns[id] = {
+ ...col,
+ label: operationDefinitionMap[col.operationType].getDefaultLabel(
+ col,
+ indexPattern,
+ copiedColumns
+ ),
+ };
+ }
+ });
+ return { ...layer, columns: copiedColumns };
+}
+
export function resetIncomplete(layer: IndexPatternLayer, columnId: string): IndexPatternLayer {
const incompleteColumns = { ...(layer.incompleteColumns ?? {}) };
delete incompleteColumns[columnId];