diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx
index 3d692b1f7f5a8..962abd8d943db 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx
@@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiHorizontalRule, EuiRadio, EuiSelect, htmlIdGenerator } from '@elastic/eui';
import { IndexPatternLayer, IndexPatternField } from '../types';
import { hasField } from '../utils';
+import { IndexPatternColumn } from '../operations';
const generator = htmlIdGenerator('lens-nesting');
@@ -21,6 +22,10 @@ function nestColumn(columnOrder: string[], outer: string, inner: string) {
return result;
}
+function getFieldName(fieldMap: Record, column: IndexPatternColumn) {
+ return hasField(column) ? fieldMap[column.sourceField]?.displayName || column.sourceField : '';
+}
+
export function BucketNestingEditor({
columnId,
layer,
@@ -39,7 +44,7 @@ export function BucketNestingEditor({
.map(([value, c]) => ({
value,
text: c.label,
- fieldName: hasField(c) ? fieldMap[c.sourceField].displayName : '',
+ fieldName: getFieldName(fieldMap, c),
operationType: c.operationType,
}));
@@ -47,7 +52,7 @@ export function BucketNestingEditor({
return null;
}
- const fieldName = hasField(column) ? fieldMap[column.sourceField].displayName : '';
+ const fieldName = getFieldName(fieldMap, column);
const prevColumn = layer.columnOrder[layer.columnOrder.indexOf(columnId) - 1];
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 bd99bd16a63a8..b0d24928b794e 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
@@ -26,7 +26,7 @@ import {
} from '../operations';
import { deleteColumn, changeColumn, updateColumnParam } from '../state_helpers';
import { FieldSelect } from './field_select';
-import { hasField } from '../utils';
+import { hasField, fieldIsInvalid } from '../utils';
import { BucketNestingEditor } from './bucket_nesting_editor';
import { IndexPattern, IndexPatternField } from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
@@ -132,6 +132,15 @@ export function DimensionEditor(props: DimensionEditorProps) {
};
});
+ const selectedColumnSourceField =
+ selectedColumn && 'sourceField' in selectedColumn ? selectedColumn.sourceField : undefined;
+
+ const currentFieldIsInvalid = useMemo(
+ () =>
+ fieldIsInvalid(selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern),
+ [selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern]
+ );
+
const sideNavItems: EuiListGroupItemProps[] = operationsWithCompatibility.map(
({ operationType, compatibleWithCurrentField }) => {
const isActive = Boolean(
@@ -271,20 +280,16 @@ export function DimensionEditor(props: DimensionEditorProps) {
defaultMessage: 'Choose a field',
})}
fullWidth
- isInvalid={Boolean(incompatibleSelectedOperationType)}
- error={
- selectedColumn && incompatibleSelectedOperationType
- ? selectedOperationDefinition?.input === 'field'
- ? i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', {
- defaultMessage: 'To use this function, select a different field.',
- })
- : i18n.translate('xpack.lens.indexPattern.chooseFieldLabel', {
- defaultMessage: 'To use this function, select a field.',
- })
- : undefined
- }
+ isInvalid={Boolean(incompatibleSelectedOperationType || currentFieldIsInvalid)}
+ error={getErrorMessage(
+ selectedColumn,
+ Boolean(incompatibleSelectedOperationType),
+ selectedOperationDefinition?.input,
+ currentFieldIsInvalid
+ )}
>
) : null}
- {!incompatibleSelectedOperationType && selectedColumn && ParamEditor && (
- <>
-
- >
- )}
+ {!currentFieldIsInvalid &&
+ !incompatibleSelectedOperationType &&
+ selectedColumn &&
+ ParamEditor && (
+ <>
+
+ >
+ )}
-
- {!incompatibleSelectedOperationType && selectedColumn && (
-
{
- setState({
- ...state,
- layers: {
- ...state.layers,
- [layerId]: {
- ...state.layers[layerId],
- columns: {
- ...state.layers[layerId].columns,
- [columnId]: {
- ...selectedColumn,
- label: value,
- customLabel: true,
+ {!currentFieldIsInvalid && (
+
+ {!incompatibleSelectedOperationType && selectedColumn && (
+ {
+ setState({
+ ...state,
+ layers: {
+ ...state.layers,
+ [layerId]: {
+ ...state.layers[layerId],
+ columns: {
+ ...state.layers[layerId].columns,
+ [columnId]: {
+ ...selectedColumn,
+ label: value,
+ customLabel: true,
+ },
},
},
},
- },
- });
- }}
- />
- )}
-
- {!hideGrouping && (
- {
- setState({
- ...state,
- layers: {
- ...state.layers,
- [props.layerId]: {
- ...state.layers[props.layerId],
- columnOrder,
+ });
+ }}
+ />
+ )}
+
+ {!hideGrouping && (
+ {
+ setState({
+ ...state,
+ layers: {
+ ...state.layers,
+ [props.layerId]: {
+ ...state.layers[props.layerId],
+ columnOrder,
+ },
},
- },
- });
- }}
- />
- )}
-
- {selectedColumn && selectedColumn.dataType === 'number' ? (
- {
- setState(
- updateColumnParam({
- state,
- layerId,
- currentColumn: selectedColumn,
- paramName: 'format',
- value: newFormat,
- })
- );
- }}
- />
- ) : null}
-
+ });
+ }}
+ />
+ )}
+
+ {selectedColumn && selectedColumn.dataType === 'number' ? (
+ {
+ setState(
+ updateColumnParam({
+ state,
+ layerId,
+ currentColumn: selectedColumn,
+ paramName: 'format',
+ value: newFormat,
+ })
+ );
+ }}
+ />
+ ) : null}
+
+ )}
);
}
+function getErrorMessage(
+ selectedColumn: IndexPatternColumn | undefined,
+ incompatibleSelectedOperationType: boolean,
+ input: 'none' | 'field' | undefined,
+ fieldInvalid: boolean
+) {
+ if (selectedColumn && 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/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
index 270f9d9f67063..d15825718682c 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
@@ -25,6 +25,7 @@ import { IndexPatternPrivateState } from '../types';
import { IndexPatternColumn } from '../operations';
import { documentField } from '../document_field';
import { OperationMetadata } from '../../types';
+import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram';
jest.mock('../loader');
jest.mock('../state_helpers');
@@ -801,6 +802,35 @@ describe('IndexPatternDimensionEditorPanel', () => {
});
});
+ it('should render invalid field if field reference is broken', () => {
+ wrapper = mount(
+
+ );
+
+ expect(wrapper.find(EuiComboBox).prop('selectedOptions')).toEqual([
+ {
+ label: 'nonexistent',
+ value: { type: 'field', field: 'nonexistent' },
+ },
+ ]);
+ });
+
it('should support selecting the operation before the field', () => {
wrapper = mount();
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx
index c4d8300722f83..6f0a9c2a86acd 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx
@@ -5,9 +5,9 @@
*/
import _ from 'lodash';
-import React, { memo } from 'react';
+import React, { memo, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiLink } from '@elastic/eui';
+import { EuiLink, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import {
@@ -22,7 +22,7 @@ import { IndexPatternColumn, OperationType } from '../indexpattern';
import { getAvailableOperationsByMetadata, buildColumn, changeField } from '../operations';
import { DimensionEditor } from './dimension_editor';
import { changeColumn } from '../state_helpers';
-import { isDraggedField, hasField } from '../utils';
+import { isDraggedField, hasField, fieldIsInvalid } from '../utils';
import { IndexPatternPrivateState, IndexPatternField } from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
import { DateRange } from '../../../common';
@@ -233,14 +233,63 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens
props: IndexPatternDimensionTriggerProps
) {
const layerId = props.layerId;
-
- const selectedColumn: IndexPatternColumn | null =
- props.state.layers[layerId].columns[props.columnId] || null;
+ const layer = props.state.layers[layerId];
+ const selectedColumn: IndexPatternColumn | null = layer.columns[props.columnId] || null;
+ const currentIndexPattern = props.state.indexPatterns[layer.indexPatternId];
+
+ const selectedColumnSourceField =
+ selectedColumn && 'sourceField' in selectedColumn ? selectedColumn.sourceField : undefined;
+ const currentFieldIsInvalid = useMemo(
+ () =>
+ fieldIsInvalid(selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern),
+ [selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern]
+ );
const { columnId, uniqueLabel } = props;
if (!selectedColumn) {
return null;
}
+
+ if (currentFieldIsInvalid) {
+ return (
+
+ {i18n.translate('xpack.lens.configure.invalidConfigTooltip', {
+ defaultMessage: 'Invalid configuration.',
+ })}
+
+ {i18n.translate('xpack.lens.configure.invalidConfigTooltipClick', {
+ defaultMessage: 'Click for more details.',
+ })}
+
+ }
+ anchorClassName="lnsLayerPanel__anchor"
+ >
+
+
+
+
+
+ {selectedColumn.label}
+
+
+
+ );
+ }
+
return (
{
onChoose: (choice: FieldChoice) => void;
onDeleteColumn: () => void;
existingFields: IndexPatternPrivateState['existingFields'];
+ fieldIsInvalid: boolean;
}
export function FieldSelect({
@@ -53,6 +54,7 @@ export function FieldSelect({
onChoose,
onDeleteColumn,
existingFields,
+ fieldIsInvalid,
...rest
}: FieldSelectProps) {
const { operationByField } = operationSupportMatrix;
@@ -171,12 +173,14 @@ export function FieldSelect({
defaultMessage: 'Field',
})}
options={(memoizedFieldOptions as unknown) as EuiComboBoxOptionOption[]}
- isInvalid={Boolean(incompatibleSelectedOperationType)}
+ isInvalid={Boolean(incompatibleSelectedOperationType || fieldIsInvalid)}
selectedOptions={
((selectedColumnOperationType && selectedColumnSourceField
? [
{
- label: fieldMap[selectedColumnSourceField].displayName,
+ label: fieldIsInvalid
+ ? selectedColumnSourceField
+ : fieldMap[selectedColumnSourceField]?.displayName,
value: { type: 'field', field: selectedColumnSourceField },
},
]
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 663d7c18bb370..80765627c1fc2 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
@@ -147,7 +147,7 @@ function testInitialState(): IndexPatternPrivateState {
// Private
operationType: 'terms',
- sourceField: 'op',
+ sourceField: 'dest',
params: {
size: 5,
orderBy: { type: 'alphabetical' },
@@ -1115,7 +1115,7 @@ describe('IndexPattern Data Source suggestions', () => {
// Private
operationType: 'terms',
- sourceField: 'op',
+ sourceField: 'dest',
params: {
size: 5,
orderBy: { type: 'alphabetical' },
@@ -1615,7 +1615,7 @@ describe('IndexPattern Data Source suggestions', () => {
isBucketed: true,
operationType: 'date_histogram',
- sourceField: 'field2',
+ sourceField: 'timestamp',
params: {
interval: 'd',
},
@@ -1626,7 +1626,7 @@ describe('IndexPattern Data Source suggestions', () => {
isBucketed: true,
operationType: 'terms',
- sourceField: 'field1',
+ sourceField: 'dest',
params: { size: 5, orderBy: { type: 'alphabetical' }, orderDirection: 'asc' },
},
id3: {
@@ -1635,7 +1635,7 @@ describe('IndexPattern Data Source suggestions', () => {
isBucketed: false,
operationType: 'avg',
- sourceField: 'field1',
+ sourceField: 'bytes',
},
},
columnOrder: ['id1', 'id2', 'id3'],
@@ -1652,6 +1652,38 @@ describe('IndexPattern Data Source suggestions', () => {
})
);
});
+
+ it('does not generate suggestions if invalid fields are referenced', () => {
+ const initialState = testInitialState();
+ const state: IndexPatternPrivateState = {
+ indexPatternRefs: [],
+ existingFields: {},
+ currentIndexPatternId: '1',
+ indexPatterns: expectedIndexPatterns,
+ isFirstExistenceFetch: false,
+ layers: {
+ first: {
+ ...initialState.layers.first,
+ columns: {
+ ...initialState.layers.first.columns,
+ col2: {
+ label: 'Top 5',
+ dataType: 'string',
+ isBucketed: true,
+
+ operationType: 'terms',
+ sourceField: 'nonExistingField',
+ params: { size: 5, orderBy: { type: 'alphabetical' }, orderDirection: 'asc' },
+ },
+ },
+ columnOrder: ['col1', 'col2'],
+ },
+ },
+ };
+
+ const suggestions = getDatasourceSuggestionsFromCurrentState(state);
+ expect(suggestions).toEqual([]);
+ });
});
});
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 f5e64149c2c76..75945529ffb34 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts
@@ -18,7 +18,7 @@ import {
} from './operations';
import { operationDefinitions } from './operations/definitions';
import { TermsIndexPatternColumn } from './operations/definitions/terms';
-import { hasField } from './utils';
+import { hasField, hasInvalidReference } from './utils';
import {
IndexPattern,
IndexPatternPrivateState,
@@ -90,6 +90,7 @@ export function getDatasourceSuggestionsForField(
indexPatternId: string,
field: IndexPatternField
): IndexPatternSugestion[] {
+ if (hasInvalidReference(state)) return [];
const layers = Object.keys(state.layers);
const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId);
@@ -380,6 +381,7 @@ function createNewLayerWithMetricAggregation(
export function getDatasourceSuggestionsFromCurrentState(
state: IndexPatternPrivateState
): Array> {
+ if (hasInvalidReference(state)) return [];
const layers = Object.entries(state.layers || {});
if (layers.length > 1) {
// Return suggestions that reduce the data to each layer individually
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts
index 374dbe77b4ca3..f1d2e7765d99f 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts
@@ -5,11 +5,13 @@
*/
import { DataType } from '../types';
+import { IndexPatternPrivateState, IndexPattern } from './types';
import { DraggedField } from './indexpattern';
import {
BaseIndexPatternColumn,
FieldBasedIndexPatternColumn,
} from './operations/definitions/column_types';
+import { operationDefinitionMap, OperationType } from './operations';
/**
* Normalizes the specified operation type. (e.g. document operations
@@ -40,3 +42,37 @@ export function isDraggedField(fieldCandidate: unknown): fieldCandidate is Dragg
'indexPatternId' in fieldCandidate
);
}
+
+export function hasInvalidReference(state: IndexPatternPrivateState) {
+ return Object.values(state.layers).some((layer) => {
+ return layer.columnOrder.some((columnId) => {
+ const column = layer.columns[columnId];
+ return (
+ hasField(column) &&
+ fieldIsInvalid(
+ column.sourceField,
+ column.operationType,
+ state.indexPatterns[layer.indexPatternId]
+ )
+ );
+ });
+ });
+}
+
+export function fieldIsInvalid(
+ sourceField: string | undefined,
+ operationType: OperationType | undefined,
+ indexPattern: IndexPattern
+) {
+ const operationDefinition = operationType && operationDefinitionMap[operationType];
+ return Boolean(
+ sourceField &&
+ operationDefinition &&
+ !indexPattern.fields.some(
+ (field) =>
+ field.name === sourceField &&
+ operationDefinition.input === 'field' &&
+ operationDefinition.getPossibleOperationForField(field) !== undefined
+ )
+ );
+}