Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lens] Fix indexpattern checks for missing references #88840

Merged
merged 13 commits into from
Jan 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -367,11 +367,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