Skip to content

Commit

Permalink
[Lens] Fix indexpattern checks for missing references (#88840)
Browse files Browse the repository at this point in the history
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
dej611 and kibanamachine authored Jan 27, 2021
1 parent b09e010 commit ba1e795
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'k
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import type { DataPublicPluginStart } from 'src/plugins/data/public';
import { OperationMetadata } from '../../types';
import { createMockedIndexPattern } from '../mocks';
import { createMockedIndexPattern, createMockedIndexPatternWithoutType } from '../mocks';
import { ReferenceEditor, ReferenceEditorProps } from './reference_editor';
import { insertOrReplaceColumn } from '../operations';
import { FieldSelect } from './field_select';
Expand Down Expand Up @@ -260,6 +260,48 @@ describe('reference editor', () => {
);
});

it("should show the sub-function as invalid if there's no field compatible with it", () => {
// This may happen for saved objects after changing the type of a field
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
currentIndexPattern={createMockedIndexPatternWithoutType('number')}
layer={{
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'avg',
sourceField: 'bytes',
},
},
}}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => true,
}}
/>
);

const subFunctionSelect = wrapper
.find('[data-test-subj="indexPattern-reference-function"]')
.first();

expect(subFunctionSelect.prop('isInvalid')).toEqual(true);
expect(subFunctionSelect.prop('selectedOptions')).toEqual(
expect.arrayContaining([
expect.objectContaining({
'data-test-subj': 'lns-indexPatternDimension-avg incompatible',
label: 'Average',
value: 'avg',
}),
])
);
});

it('should hide the function selector when using a field-only selection style', () => {
wrapper = mount(
<ReferenceEditor
Expand Down Expand Up @@ -367,6 +409,38 @@ describe('reference editor', () => {
expect(fieldSelect.prop('markAllFieldsCompatible')).toEqual(true);
});

it('should show the FieldSelect as invalid if the selected field is missing', () => {
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
layer={{
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of missing',
dataType: 'number',
isBucketed: false,
operationType: 'avg',
sourceField: 'missing',
},
},
}}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => true,
}}
/>
);

const fieldSelect = wrapper.find(FieldSelect);
expect(fieldSelect.prop('fieldIsInvalid')).toEqual(true);
expect(fieldSelect.prop('selectedField')).toEqual('missing');
expect(fieldSelect.prop('selectedOperationType')).toEqual('avg');
expect(fieldSelect.prop('incompleteOperation')).toBeUndefined();
expect(fieldSelect.prop('markAllFieldsCompatible')).toEqual(false);
});

it('should show the ParamEditor for functions that offer one', () => {
wrapper = mount(
<ReferenceEditor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,19 +191,39 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
return;
}

const selectedOption = incompleteInfo?.operationType
? [functionOptions.find(({ value }) => value === incompleteInfo.operationType)!]
const selectedOption = incompleteOperation
? [functionOptions.find(({ value }) => value === incompleteOperation)!]
: column
? [functionOptions.find(({ value }) => value === column.operationType)!]
: [];

// If the operationType is incomplete, the user needs to select a field- so
// the function is marked as valid.
const showOperationInvalid = !column && !Boolean(incompleteInfo?.operationType);
const showOperationInvalid = !column && !Boolean(incompleteOperation);
// The field is invalid if the operation has been updated without a field,
// or if we are in a field-only mode but empty state
const showFieldInvalid =
Boolean(incompleteInfo?.operationType) || (selectionStyle === 'field' && !column);
const showFieldInvalid = Boolean(incompleteOperation) || (selectionStyle === 'field' && !column);
// Check if the field still exists to protect from changes
const showFieldMissingInvalid = !currentIndexPattern.getFieldByName(
incompleteField ?? (column as FieldBasedIndexPatternColumn)?.sourceField
);

// what about a field changing type and becoming invalid?
// Let's say this change makes the indexpattern without any number field but the operation was set to a numeric operation.
// At this point the ComboBox will crash.
// Therefore check if the selectedOption is in functionOptions and in case fill it in as disabled option
const showSelectionFunctionInvalid = Boolean(selectedOption.length && selectedOption[0] == null);
if (showSelectionFunctionInvalid) {
const selectedOperationType = incompleteOperation || column.operationType;
const brokenFunctionOption = {
label: operationPanels[selectedOperationType].displayName,
value: selectedOperationType,
className: 'lnsIndexPatternDimensionEditor__operation',
'data-test-subj': `lns-indexPatternDimension-${selectedOperationType} incompatible`,
};
functionOptions.push(brokenFunctionOption);
selectedOption[0] = brokenFunctionOption;
}

return (
<div id={columnId}>
Expand All @@ -216,7 +236,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
defaultMessage: 'Choose a sub-function',
})}
fullWidth
isInvalid={showOperationInvalid}
isInvalid={showOperationInvalid || showSelectionFunctionInvalid}
>
<EuiComboBox
fullWidth
Expand All @@ -230,7 +250,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
}
)}
options={functionOptions}
isInvalid={showOperationInvalid}
isInvalid={showOperationInvalid || showSelectionFunctionInvalid}
selectedOptions={selectedOption}
singleSelection={{ asPlainText: true }}
onChange={(choices) => {
Expand Down Expand Up @@ -258,11 +278,11 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
defaultMessage: 'Select a field',
})}
fullWidth
isInvalid={showFieldInvalid}
isInvalid={showFieldInvalid || showFieldMissingInvalid}
labelAppend={labelAppend}
>
<FieldSelect
fieldIsInvalid={showFieldInvalid}
fieldIsInvalid={showFieldInvalid || showFieldMissingInvalid}
currentIndexPattern={currentIndexPattern}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
Expand All @@ -272,9 +292,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
}
selectedField={
// Allows field to be selected
incompleteField
? incompleteField
: (column as FieldBasedIndexPatternColumn)?.sourceField
incompleteField ?? (column as FieldBasedIndexPatternColumn)?.sourceField
}
incompleteOperation={incompleteOperation}
markAllFieldsCompatible={selectionStyle === 'field'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,11 +370,14 @@ export function getIndexPatternDatasource({
return;
}

// Forward the indexpattern as well, as it is required by some operationType checks
const layerErrors = Object.values(state.layers).map((layer) =>
(getErrorMessages(layer) ?? []).map((message) => ({
shortMessage: '', // Not displayed currently
longMessage: message,
}))
(getErrorMessages(layer, state.indexPatterns[layer.indexPatternId]) ?? []).map(
(message) => ({
shortMessage: '', // Not displayed currently
longMessage: message,
})
)
);

// Single layer case, no need to explain more
Expand Down
78 changes: 77 additions & 1 deletion x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,83 @@

import { DragContextState } from '../drag_drop';
import { getFieldByNameFactory } from './pure_helpers';
import type { IndexPattern } from './types';
import type { IndexPattern, IndexPatternField } from './types';

export const createMockedIndexPatternWithoutType = (
typeToFilter: IndexPatternField['type']
): IndexPattern => {
const fields = [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
},
{
name: 'start_date',
displayName: 'start_date',
type: 'date',
aggregatable: true,
searchable: true,
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'memory',
displayName: 'memory',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
esTypes: ['keyword'],
},
{
name: 'unsupported',
displayName: 'unsupported',
type: 'geo',
aggregatable: true,
searchable: true,
},
{
name: 'dest',
displayName: 'dest',
type: 'string',
aggregatable: true,
searchable: true,
esTypes: ['keyword'],
},
{
name: 'scripted',
displayName: 'Scripted',
type: 'string',
searchable: true,
aggregatable: true,
scripted: true,
lang: 'painless',
script: '1234',
},
].filter(({ type }) => type !== typeToFilter);
return {
id: '1',
title: 'my-fake-index-pattern',
timeFieldName: 'timestamp',
hasRestrictions: false,
fields,
getFieldByName: getFieldByNameFactory(fields),
};
};

export const createMockedIndexPattern = (): IndexPattern => {
const fields = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn> {
getErrorMessage?: (
layer: IndexPatternLayer,
columnId: string,
indexPattern?: IndexPattern
indexPattern: IndexPattern
) => string[] | undefined;

/*
Expand Down Expand Up @@ -301,7 +301,7 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> {
getErrorMessage: (
layer: IndexPatternLayer,
columnId: string,
indexPattern?: IndexPattern
indexPattern: IndexPattern
) => string[] | undefined;
}

Expand Down
Loading

0 comments on commit ba1e795

Please sign in to comment.