[Lens] Fix multi terms fields validation (#126618)

* 🐛 Fix fields validation for multi terms

* 🐛 Fix unit tests + add some new ones

* 🌐 Removed unused translations

* Revert "🌐 Removed unused translations"

This reverts commit a94ee699c0.

* 🌐 Removed unused translations

* 🐛 Extends deeper validation to drag and drop as well + more tests

* 🐛 Fix last issue with refactored function

*  Filtered fields with unsupported types when in multi mode

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marco Liberati 2022-03-09 11:34:47 +01:00 committed by GitHub
parent f74c894c5d
commit f5556f1327
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 435 additions and 128 deletions

View file

@ -134,7 +134,7 @@ function getDropPropsForField({
const isTheSameIndexPattern = state.layers[layerId].indexPatternId === dragging.indexPatternId;
const newOperation = getNewOperation(dragging.field, filterOperations, targetColumn);
if (!!(isTheSameIndexPattern && newOperation)) {
if (isTheSameIndexPattern && newOperation) {
const nextLabel = operationLabels[newOperation].displayName;
if (!targetColumn) {

View file

@ -7,6 +7,7 @@
import { createMockedIndexPattern } from '../../mocks';
import { getInvalidFieldMessage } from './helpers';
import type { TermsIndexPatternColumn } from './terms';
describe('helpers', () => {
describe('getInvalidFieldMessage', () => {
@ -16,13 +17,13 @@ describe('helpers', () => {
dataType: 'number',
isBucketed: false,
label: 'Foo',
operationType: 'count', // <= invalid
sourceField: 'bytes',
operationType: 'count',
sourceField: 'NoBytes', // <= invalid
},
createMockedIndexPattern()
);
expect(messages).toHaveLength(1);
expect(messages![0]).toEqual('Field bytes was not found');
expect(messages![0]).toEqual('Field NoBytes was not found');
});
it('returns an error if a field is the wrong type', () => {
@ -31,8 +32,8 @@ describe('helpers', () => {
dataType: 'number',
isBucketed: false,
label: 'Foo',
operationType: 'average', // <= invalid
sourceField: 'timestamp',
operationType: 'average',
sourceField: 'timestamp', // <= invalid type for average
},
createMockedIndexPattern()
);
@ -40,6 +41,78 @@ describe('helpers', () => {
expect(messages![0]).toEqual('Field timestamp is of the wrong type');
});
it('returns an error if one field amongst multiples does not exist', () => {
const messages = getInvalidFieldMessage(
{
dataType: 'number',
isBucketed: false,
label: 'Foo',
operationType: 'terms',
sourceField: 'geo.src',
params: {
secondaryFields: ['NoBytes'], // <= field does not exist
},
} as TermsIndexPatternColumn,
createMockedIndexPattern()
);
expect(messages).toHaveLength(1);
expect(messages![0]).toEqual('Field NoBytes was not found');
});
it('returns an error if multiple fields do not exist', () => {
const messages = getInvalidFieldMessage(
{
dataType: 'number',
isBucketed: false,
label: 'Foo',
operationType: 'terms',
sourceField: 'NotExisting',
params: {
secondaryFields: ['NoBytes'], // <= field does not exist
},
} as TermsIndexPatternColumn,
createMockedIndexPattern()
);
expect(messages).toHaveLength(1);
expect(messages![0]).toEqual('Fields NotExisting, NoBytes were not found');
});
it('returns an error if one field amongst multiples has the wrong type', () => {
const messages = getInvalidFieldMessage(
{
dataType: 'number',
isBucketed: false,
label: 'Foo',
operationType: 'terms',
sourceField: 'geo.src',
params: {
secondaryFields: ['timestamp'], // <= invalid type
},
} as TermsIndexPatternColumn,
createMockedIndexPattern()
);
expect(messages).toHaveLength(1);
expect(messages![0]).toEqual('Field timestamp is of the wrong type');
});
it('returns an error if multiple fields are of the wrong type', () => {
const messages = getInvalidFieldMessage(
{
dataType: 'number',
isBucketed: false,
label: 'Foo',
operationType: 'terms',
sourceField: 'start_date', // <= invalid type
params: {
secondaryFields: ['timestamp'], // <= invalid type
},
} as TermsIndexPatternColumn,
createMockedIndexPattern()
);
expect(messages).toHaveLength(1);
expect(messages![0]).toEqual('Fields start_date, timestamp are of the wrong type');
});
it('returns no message if all fields are matching', () => {
const messages = getInvalidFieldMessage(
{

View file

@ -12,7 +12,8 @@ import {
FormattedIndexPatternColumn,
ReferenceBasedIndexPatternColumn,
} from './column_types';
import { IndexPattern } from '../../types';
import { IndexPattern, IndexPatternField } from '../../types';
import { hasField } from '../../pure_utils';
export function getInvalidFieldMessage(
column: FieldBasedIndexPatternColumn,
@ -21,47 +22,66 @@ export function getInvalidFieldMessage(
if (!indexPattern) {
return;
}
const { sourceField, operationType } = column;
const field = sourceField ? indexPattern.getFieldByName(sourceField) : undefined;
const operationDefinition = operationType && operationDefinitionMap[operationType];
const { operationType } = column;
const operationDefinition = operationType ? operationDefinitionMap[operationType] : undefined;
const fieldNames =
hasField(column) && operationDefinition
? operationDefinition?.getCurrentFields?.(column) ?? [column.sourceField]
: [];
const fields = fieldNames.map((fieldName) => indexPattern.getFieldByName(fieldName));
const filteredFields = fields.filter(Boolean) as IndexPatternField[];
const isInvalid = Boolean(
sourceField &&
operationDefinition &&
fields.length > filteredFields.length ||
!(
field &&
operationDefinition?.input === 'field' &&
operationDefinition.getPossibleOperationForField(field) !== undefined
filteredFields.every(
(field) => operationDefinition.getPossibleOperationForField(field) != null
)
)
);
const isWrongType = Boolean(
sourceField &&
operationDefinition &&
field &&
!operationDefinition.isTransferable(
filteredFields.length &&
!operationDefinition?.isTransferable(
column as GenericIndexPatternColumn,
indexPattern,
operationDefinitionMap
)
);
if (isInvalid) {
if (isWrongType) {
// Missing fields have priority over wrong type
// This has been moved as some transferable checks also perform exist checks internally and fail eventually
// but that would make type mismatch error appear in place of missing fields scenarios
const missingFields = fields.map((field, i) => (field ? null : fieldNames[i])).filter(Boolean);
if (missingFields.length) {
return [
i18n.translate('xpack.lens.indexPattern.fieldWrongType', {
defaultMessage: 'Field {invalidField} is of the wrong type',
i18n.translate('xpack.lens.indexPattern.fieldsNotFound', {
defaultMessage:
'{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found',
values: {
invalidField: sourceField,
count: missingFields.length,
missingFields: missingFields.join(', '),
},
}),
];
}
if (isWrongType) {
// as fallback show all the fields as invalid?
const wrongTypeFields =
operationDefinition?.getNonTransferableFields?.(column, indexPattern) ?? fieldNames;
return [
i18n.translate('xpack.lens.indexPattern.fieldsWrongType', {
defaultMessage:
'{count, plural, one {Field} other {Fields}} {invalidFields} {count, plural, one {is} other {are}} of the wrong type',
values: {
count: wrongTypeFields.length,
invalidFields: wrongTypeFields.join(', '),
},
}),
];
}
return [
i18n.translate('xpack.lens.indexPattern.fieldNotFound', {
defaultMessage: 'Field {invalidField} was not found',
values: { invalidField: sourceField },
}),
];
}
return undefined;

View file

@ -336,6 +336,12 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn, P = {}>
* Operation can influence some visual default settings. This function is used to collect default values offered
*/
getDefaultVisualSettings?: (column: C) => { truncateText?: boolean };
/**
* Utility function useful for multi fields operation in order to get fields
* are not pass the transferable checks
*/
getNonTransferableFields?: (column: C, indexPattern: IndexPattern) => string[];
}
interface BaseBuildColumnArgs {

View file

@ -0,0 +1,14 @@
/*
* 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 const DEFAULT_SIZE = 3;
// Elasticsearch limit
export const MAXIMUM_MAX_DOC_COUNT = 100;
export const DEFAULT_MAX_DOC_COUNT = 1;
export const supportedTypes = new Set(['string', 'boolean', 'number', 'ip']);
export const MULTI_KEY_VISUAL_SEPARATOR = '';

View file

@ -21,6 +21,7 @@ import { FieldSelect } from '../../../dimension_panel/field_select';
import type { TermsIndexPatternColumn } from './types';
import type { IndexPattern, IndexPatternPrivateState } from '../../../types';
import type { OperationSupportMatrix } from '../../../dimension_panel';
import { supportedTypes } from './constants';
const generateId = htmlIdGenerator();
export const MAX_MULTI_FIELDS_SIZE = 3;
@ -29,6 +30,7 @@ export interface FieldInputsProps {
column: TermsIndexPatternColumn;
indexPattern: IndexPattern;
existingFields: IndexPatternPrivateState['existingFields'];
invalidFields?: string[];
operationSupportMatrix: Pick<OperationSupportMatrix, 'operationByField'>;
onChange: (newValues: string[]) => void;
}
@ -51,6 +53,7 @@ export function FieldInputs({
indexPattern,
existingFields,
operationSupportMatrix,
invalidFields,
}: FieldInputsProps) {
const onChangeWrapped = useCallback(
(values: WrappedValue[]) =>
@ -90,7 +93,7 @@ export function FieldInputs({
return (
<>
<FieldSelect
fieldIsInvalid={false}
fieldIsInvalid={Boolean(invalidFields?.[0])}
currentIndexPattern={indexPattern}
existingFields={existingFields}
operationByField={operationSupportMatrix.operationByField}
@ -129,21 +132,31 @@ export function FieldInputs({
{localValues.map(({ id, value, isNew }, index) => {
// need to filter the available fields for multiple terms
// * a scripted field should be removed
// * if a field has been used, should it be removed? Probably yes?
// * if a scripted field was used in a singular term, should it be marked as invalid for multi-terms? Probably yes?
// * a field of unsupported type should be removed
// * a field that has been used
// * a scripted field was used in a singular term, should be marked as invalid for multi-terms
const filteredOperationByField = Object.keys(operationSupportMatrix.operationByField)
.filter(
(key) =>
(!rawValuesLookup.has(key) && !indexPattern.getFieldByName(key)?.scripted) ||
key === value
)
.filter((key) => {
if (key === value) {
return true;
}
const field = indexPattern.getFieldByName(key);
return (
!rawValuesLookup.has(key) &&
field &&
!field.scripted &&
supportedTypes.has(field.type)
);
})
.reduce<OperationSupportMatrix['operationByField']>((memo, key) => {
memo[key] = operationSupportMatrix.operationByField[key];
return memo;
}, {});
const shouldShowScriptedFieldError = Boolean(
value && indexPattern.getFieldByName(value)?.scripted && localValuesFilled.length > 1
const shouldShowError = Boolean(
value &&
((indexPattern.getFieldByName(value)?.scripted && localValuesFilled.length > 1) ||
invalidFields?.includes(value))
);
return (
<EuiDraggable
@ -170,7 +183,7 @@ export function FieldInputs({
</EuiFlexItem>
<EuiFlexItem grow={true} style={{ minWidth: 0 }}>
<FieldSelect
fieldIsInvalid={false}
fieldIsInvalid={shouldShowError}
currentIndexPattern={indexPattern}
existingFields={existingFields}
operationByField={filteredOperationByField}
@ -180,7 +193,7 @@ export function FieldInputs({
onChoose={(choice) => {
onFieldSelectChange(choice, index);
}}
isInvalid={shouldShowScriptedFieldError}
isInvalid={shouldShowError}
data-test-subj={`indexPattern-dimension-field-${index}`}
/>
</EuiFlexItem>
@ -233,3 +246,21 @@ export function FieldInputs({
</>
);
}
export function getInputFieldErrorMessage(isScriptedField: boolean, invalidFields: string[]) {
if (isScriptedField) {
return i18n.translate('xpack.lens.indexPattern.terms.scriptedFieldErrorShort', {
defaultMessage: 'Scripted fields are not supported when using multiple fields',
});
}
if (invalidFields.length) {
return i18n.translate('xpack.lens.indexPattern.terms.invalidFieldsErrorShort', {
defaultMessage:
'Invalid {invalidFieldsCount, plural, one {field} other {fields}}: {invalidFields}. Check your data view or pick another field.',
values: {
invalidFieldsCount: invalidFields.length,
invalidFields: invalidFields.map((fieldName) => `"${fieldName}"`).join(', '),
},
});
}
}

View file

@ -15,9 +15,9 @@ import {
getDisallowedTermsMessage,
getMultiTermsScriptedFieldErrorMessage,
isSortableByColumn,
MULTI_KEY_VISUAL_SEPARATOR,
} from './helpers';
import { ReferenceBasedIndexPatternColumn } from '../column_types';
import { MULTI_KEY_VISUAL_SEPARATOR } from './constants';
const indexPattern = createMockedIndexPattern();

View file

@ -10,7 +10,7 @@ import { uniq } from 'lodash';
import type { CoreStart } from 'kibana/public';
import { buildEsQuery } from '@kbn/es-query';
import { getEsQueryConfig } from '../../../../../../../../src/plugins/data/public';
import { operationDefinitionMap } from '../index';
import { GenericIndexPatternColumn, operationDefinitionMap } from '../index';
import { defaultLabel } from '../filters';
import { isReferenced } from '../../layer_helpers';
@ -18,9 +18,9 @@ import type { FieldStatsResponse } from '../../../../../common';
import type { FrameDatasourceAPI } from '../../../../types';
import type { FiltersIndexPatternColumn } from '../index';
import type { TermsIndexPatternColumn } from './types';
import type { IndexPatternLayer, IndexPattern } from '../../../types';
export const MULTI_KEY_VISUAL_SEPARATOR = '';
import type { IndexPatternLayer, IndexPattern, IndexPatternField } from '../../../types';
import { MULTI_KEY_VISUAL_SEPARATOR, supportedTypes } from './constants';
import { isColumnOfType } from '../helpers';
const fullSeparatorString = ` ${MULTI_KEY_VISUAL_SEPARATOR} `;
@ -213,3 +213,63 @@ export function isSortableByColumn(layer: IndexPatternLayer, columnId: string) {
!isReferenced(layer, columnId)
);
}
export function isScriptedField(field: IndexPatternField): boolean;
export function isScriptedField(fieldName: string, indexPattern: IndexPattern): boolean;
export function isScriptedField(
fieldName: string | IndexPatternField,
indexPattern?: IndexPattern
) {
if (typeof fieldName === 'string') {
const field = indexPattern?.getFieldByName(fieldName);
return field && field.scripted;
}
return fieldName.scripted;
}
export function getFieldsByValidationState(
newIndexPattern: IndexPattern,
column?: GenericIndexPatternColumn,
field?: string | IndexPatternField
): {
allFields: Array<IndexPatternField | undefined>;
validFields: string[];
invalidFields: string[];
} {
const newFieldNames: string[] = [];
if (column && 'sourceField' in column) {
if (column.sourceField) {
newFieldNames.push(column.sourceField);
}
if (isColumnOfType<TermsIndexPatternColumn>('terms', column)) {
newFieldNames.push(...(column.params?.secondaryFields ?? []));
}
}
if (field) {
newFieldNames.push(typeof field === 'string' ? field : field.name || field.displayName);
}
const newFields = newFieldNames.map((fieldName) => newIndexPattern.getFieldByName(fieldName));
// lodash groupby does not provide the index arg, so had to write it manually :(
const validFields: string[] = [];
const invalidFields: string[] = [];
// mind to check whether a column was passed, in such case single term with scripted field is ok
const canAcceptScripted = Boolean(column && newFields.length === 1);
newFieldNames.forEach((fieldName, i) => {
const newField = newFields[i];
const isValid =
newField &&
supportedTypes.has(newField.type) &&
newField.aggregatable &&
(!newField.aggregationRestrictions || newField.aggregationRestrictions.terms) &&
(canAcceptScripted || !isScriptedField(newField));
const arrayToPush = isValid ? validFields : invalidFields;
arrayToPush.push(fieldName);
});
return {
allFields: newFields,
validFields,
invalidFields,
};
}

View file

@ -26,19 +26,26 @@ import type { DataType } from '../../../../types';
import { OperationDefinition } from '../index';
import { FieldBasedIndexPatternColumn } from '../column_types';
import { ValuesInput } from './values_input';
import { getInvalidFieldMessage, isColumnOfType } from '../helpers';
import { FieldInputs, MAX_MULTI_FIELDS_SIZE } from './field_inputs';
import { getInvalidFieldMessage } from '../helpers';
import { FieldInputs, getInputFieldErrorMessage, MAX_MULTI_FIELDS_SIZE } from './field_inputs';
import {
FieldInput as FieldInputBase,
getErrorMessage,
} from '../../../dimension_panel/field_input';
import type { TermsIndexPatternColumn } from './types';
import type { IndexPattern, IndexPatternField } from '../../../types';
import type { IndexPatternField } from '../../../types';
import {
getDisallowedTermsMessage,
getMultiTermsScriptedFieldErrorMessage,
getFieldsByValidationState,
isSortableByColumn,
} from './helpers';
import {
DEFAULT_MAX_DOC_COUNT,
DEFAULT_SIZE,
MAXIMUM_MAX_DOC_COUNT,
supportedTypes,
} from './constants';
export function supportsRarityRanking(field?: IndexPatternField) {
// these es field types can't be sorted by rarity
@ -79,27 +86,12 @@ function ofName(name?: string, count: number = 0, rare: boolean = false) {
});
}
function isScriptedField(field: IndexPatternField): boolean;
function isScriptedField(fieldName: string, indexPattern: IndexPattern): boolean;
function isScriptedField(fieldName: string | IndexPatternField, indexPattern?: IndexPattern) {
if (typeof fieldName === 'string') {
const field = indexPattern?.getFieldByName(fieldName);
return field && field.scripted;
}
return fieldName.scripted;
}
// It is not always possible to know if there's a numeric field, so just ignore it for now
function getParentFormatter(params: Partial<TermsIndexPatternColumn['params']>) {
return { id: params.secondaryFields?.length ? 'multi_terms' : 'terms' };
}
const idPrefix = htmlIdGenerator()();
const DEFAULT_SIZE = 3;
// Elasticsearch limit
const MAXIMUM_MAX_DOC_COUNT = 100;
export const DEFAULT_MAX_DOC_COUNT = 1;
const supportedTypes = new Set(['string', 'boolean', 'number', 'ip']);
export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field'> = {
type: 'terms',
@ -112,30 +104,18 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
return [targetColumn.sourceField, ...(targetColumn?.params?.secondaryFields ?? [])];
},
getParamsForMultipleFields: ({ targetColumn, sourceColumn, field, indexPattern }) => {
const secondaryFields = new Set<string>();
if (targetColumn.params?.secondaryFields?.length) {
targetColumn.params.secondaryFields.forEach((fieldName) => {
if (!isScriptedField(fieldName, indexPattern)) {
secondaryFields.add(fieldName);
}
});
}
if (sourceColumn && 'sourceField' in sourceColumn && sourceColumn?.sourceField) {
if (!isScriptedField(sourceColumn.sourceField, indexPattern)) {
secondaryFields.add(sourceColumn.sourceField);
}
}
if (sourceColumn && isColumnOfType<TermsIndexPatternColumn>('terms', sourceColumn)) {
if (sourceColumn?.params?.secondaryFields?.length) {
sourceColumn.params.secondaryFields.forEach((fieldName) => {
if (!isScriptedField(fieldName, indexPattern)) {
secondaryFields.add(fieldName);
}
});
}
}
if (field && !isScriptedField(field)) {
secondaryFields.add(field.name);
const secondaryFields = new Set<string>(
getFieldsByValidationState(indexPattern, targetColumn).validFields
);
const validFieldsToAdd = getFieldsByValidationState(
indexPattern,
sourceColumn,
field
).validFields;
for (const validField of validFieldsToAdd) {
secondaryFields.add(validField);
}
// remove the sourceField
secondaryFields.delete(targetColumn.sourceField);
@ -155,27 +135,12 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
return false;
}
// collect the fields from the targetColumn
const originalTerms = new Set([
targetColumn.sourceField,
...(targetColumn.params?.secondaryFields ?? []),
]);
const originalTerms = new Set(
getFieldsByValidationState(indexPattern, targetColumn).validFields
);
// now check how many fields can be added
let counter = field && !isScriptedField(field) && !originalTerms.has(field.name) ? 1 : 0;
if (sourceColumn) {
if ('sourceField' in sourceColumn) {
counter +=
!isScriptedField(sourceColumn.sourceField, indexPattern) &&
!originalTerms.has(sourceColumn.sourceField)
? 1
: 0;
if (isColumnOfType<TermsIndexPatternColumn>('terms', sourceColumn)) {
counter +=
sourceColumn.params.secondaryFields?.filter((f) => {
return !isScriptedField(f, indexPattern) && !originalTerms.has(f);
}).length ?? 0;
}
}
}
const { validFields } = getFieldsByValidationState(indexPattern, sourceColumn, field);
const counter = validFields.filter((fieldName) => !originalTerms.has(fieldName)).length;
// reject when there are no new fields to add
if (!counter) {
return false;
@ -209,14 +174,15 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
].filter(Boolean);
return messages.length ? messages : undefined;
},
getNonTransferableFields: (column, newIndexPattern) => {
return getFieldsByValidationState(newIndexPattern, column).invalidFields;
},
isTransferable: (column, newIndexPattern) => {
const newField = newIndexPattern.getFieldByName(column.sourceField);
const { allFields, invalidFields } = getFieldsByValidationState(newIndexPattern, column);
return Boolean(
newField &&
supportedTypes.has(newField.type) &&
newField.aggregatable &&
(!newField.aggregationRestrictions || newField.aggregationRestrictions.terms) &&
allFields.length &&
invalidFields.length === 0 &&
(!column.params.otherBucket || !newIndexPattern.hasRestrictions)
);
},
@ -446,6 +412,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
const showScriptedFieldError = Boolean(
getMultiTermsScriptedFieldErrorMessage(layer, columnId, indexPattern)
);
const { invalidFields } = getFieldsByValidationState(indexPattern, selectedColumn);
return (
<EuiFormRow
@ -457,14 +424,8 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
},
})}
fullWidth
isInvalid={Boolean(showScriptedFieldError)}
error={
showScriptedFieldError
? i18n.translate('xpack.lens.indexPattern.terms.scriptedFieldErrorShort', {
defaultMessage: 'Scripted fields are not supported when using multiple fields',
})
: []
}
isInvalid={Boolean(showScriptedFieldError || invalidFields.length)}
error={getInputFieldErrorMessage(showScriptedFieldError, invalidFields)}
>
<FieldInputs
column={selectedColumn}
@ -472,6 +433,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
onChange={onFieldSelectChange}
invalidFields={invalidFields}
/>
</EuiFormRow>
);

View file

@ -59,7 +59,8 @@ const defaultProps = {
data: dataPluginMock.createStartContract(),
http: {} as HttpSetup,
indexPattern: createMockedIndexPattern(),
operationDefinitionMap: {},
// need to provide the terms operation as some helpers use operation specific features
operationDefinitionMap: { terms: termsOperation as unknown as GenericOperationDefinition },
isFullscreen: false,
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
@ -1016,7 +1017,7 @@ describe('terms', () => {
).toBeTruthy();
});
it('should show an error message when field is invalid', () => {
it('should show an error message when first field is invalid', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
@ -1049,7 +1050,7 @@ describe('terms', () => {
).toBe('Invalid field. Check your data view or pick another field.');
});
it('should show an error message when field is not supported', () => {
it('should show an error message when first field is not supported', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
@ -1083,6 +1084,74 @@ describe('terms', () => {
).toBe('This field does not work with the selected function.');
});
it('should show an error message when any field but the first is invalid', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
layer.columns.col1 = {
label: 'Top value of geo.src + 1 other',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
params: {
orderBy: { type: 'alphabetical' },
size: 3,
orderDirection: 'asc',
secondaryFields: ['unsupported'],
},
sourceField: 'geo.src',
} as TermsIndexPatternColumn;
const instance = mount(
<InlineFieldInput
{...defaultFieldInputProps}
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
);
expect(
instance.find('[data-test-subj="indexPattern-field-selection-row"]').first().prop('error')
).toBe('Invalid field: "unsupported". Check your data view or pick another field.');
});
it('should show an error message when any field but the first is not supported', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
layer.columns.col1 = {
label: 'Top value of geo.src + 1 other',
dataType: 'date',
isBucketed: true,
operationType: 'terms',
params: {
orderBy: { type: 'alphabetical' },
size: 3,
orderDirection: 'asc',
secondaryFields: ['timestamp'],
},
sourceField: 'geo.src',
} as TermsIndexPatternColumn;
const instance = mount(
<InlineFieldInput
{...defaultFieldInputProps}
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
);
expect(
instance.find('[data-test-subj="indexPattern-field-selection-row"]').first().prop('error')
).toBe('Invalid field: "timestamp". Check your data view or pick another field.');
});
it('should render the an add button for single layer, but no other hints', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
@ -1370,6 +1439,38 @@ describe('terms', () => {
);
});
it('should filter fields with unsupported types when in multi terms mode', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
(layer.columns.col1 as TermsIndexPatternColumn).params.secondaryFields = ['memory'];
const instance = mount(
<InlineFieldInput
{...defaultFieldInputProps}
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
);
// get inner instance
expect(
instance.find('[data-test-subj="indexPattern-dimension-field-0"]').at(1).prop('options')
).toEqual(
expect.arrayContaining([
expect.objectContaining({
options: expect.arrayContaining([
expect.not.objectContaining({ 'data-test-subj': 'lns-fieldOption-timestamp' }),
]),
}),
])
);
});
it('should limit the number of multiple fields', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
@ -2350,4 +2451,47 @@ describe('terms', () => {
});
});
});
describe('getNonTransferableFields', () => {
it('should return empty array if all fields are transferable', () => {
expect(
termsOperation.getNonTransferableFields?.(
createMultiTermsColumn(['source']),
defaultProps.indexPattern
)
).toEqual([]);
expect(
termsOperation.getNonTransferableFields?.(
createMultiTermsColumn(['source', 'bytes']),
defaultProps.indexPattern
)
).toEqual([]);
expect(
termsOperation.getNonTransferableFields?.(
createMultiTermsColumn([]),
defaultProps.indexPattern
)
).toEqual([]);
expect(
termsOperation.getNonTransferableFields?.(
createMultiTermsColumn(['source', 'geo.src']),
defaultProps.indexPattern
)
).toEqual([]);
});
it('should return only non transferable fields (invalid or not existence)', () => {
expect(
termsOperation.getNonTransferableFields?.(
createMultiTermsColumn(['source', 'timestamp']),
defaultProps.indexPattern
)
).toEqual(['timestamp']);
expect(
termsOperation.getNonTransferableFields?.(
createMultiTermsColumn(['source', 'unsupported']),
defaultProps.indexPattern
)
).toEqual(['unsupported']);
});
});
});

View file

@ -33,7 +33,8 @@ import { FiltersIndexPatternColumn, isQueryValid } from './operations/definition
import { checkColumnForPrecisionError, Query } from '../../../../../src/plugins/data/common';
import { hasField } from './pure_utils';
import { mergeLayer } from './state_helpers';
import { DEFAULT_MAX_DOC_COUNT, supportsRarityRanking } from './operations/definitions/terms';
import { supportsRarityRanking } from './operations/definitions/terms';
import { DEFAULT_MAX_DOC_COUNT } from './operations/definitions/terms/constants';
import { getOriginalId } from '../../common/expressions';
export function isColumnInvalid(

View file

@ -468,7 +468,6 @@
"xpack.lens.indexPattern.fieldItem.visualizeGeoFieldLinkText": "Mapsで可視化",
"xpack.lens.indexPattern.fieldItemTooltip": "可視化するには、ドラッグアンドドロップします。",
"xpack.lens.indexPattern.fieldNoOperation": "フィールド{field}は演算なしで使用できません",
"xpack.lens.indexPattern.fieldNotFound": "フィールド {invalidField} が見つかりませんでした",
"xpack.lens.indexPattern.fieldPanelEmptyStringValue": "空の文字列",
"xpack.lens.indexPattern.fieldPlaceholder": "フィールド",
"xpack.lens.indexPattern.fieldStatsButtonAriaLabel": "プレビュー{fieldName}{fieldType}",
@ -480,7 +479,6 @@
"xpack.lens.indexPattern.fieldStatsNoData": "このフィールドは空です。500件のサンプリングされたドキュメントに存在しません。このフィールドを構成に追加すると、空白のグラフが作成される場合があります。",
"xpack.lens.indexPattern.fieldTimeDistributionLabel": "時間分布",
"xpack.lens.indexPattern.fieldTopValuesLabel": "トップの値",
"xpack.lens.indexPattern.fieldWrongType": "フィールド{invalidField}の型が正しくありません",
"xpack.lens.indexPattern.filterBy.clickToEdit": "クリックして編集",
"xpack.lens.indexPattern.filterBy.emptyFilterQuery": "(空)",
"xpack.lens.indexPattern.filterBy.label": "フィルタリング条件",

View file

@ -473,7 +473,6 @@
"xpack.lens.indexPattern.fieldItem.visualizeGeoFieldLinkText": "在 Maps 中可视化",
"xpack.lens.indexPattern.fieldItemTooltip": "拖放以可视化。",
"xpack.lens.indexPattern.fieldNoOperation": "没有运算,无法使用字段 {field}",
"xpack.lens.indexPattern.fieldNotFound": "未找到字段 {invalidField}",
"xpack.lens.indexPattern.fieldPanelEmptyStringValue": "空字符串",
"xpack.lens.indexPattern.fieldPlaceholder": "字段",
"xpack.lens.indexPattern.fieldStatsButtonAriaLabel": "预览 {fieldName}{fieldType}",
@ -485,7 +484,6 @@
"xpack.lens.indexPattern.fieldStatsNoData": "此字段为空,因为它不存在于 500 个采样文档中。将此字段添加到配置可能会导致空图表。",
"xpack.lens.indexPattern.fieldTimeDistributionLabel": "时间分布",
"xpack.lens.indexPattern.fieldTopValuesLabel": "排名最前值",
"xpack.lens.indexPattern.fieldWrongType": "字段 {invalidField} 的类型不正确",
"xpack.lens.indexPattern.filterBy.clickToEdit": "单击以编辑",
"xpack.lens.indexPattern.filterBy.emptyFilterQuery": "(空)",
"xpack.lens.indexPattern.filterBy.label": "筛选依据",