mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
f74c894c5d
commit
f5556f1327
13 changed files with 435 additions and 128 deletions
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = '›';
|
|
@ -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(', '),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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": "フィルタリング条件",
|
||||
|
|
|
@ -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": "筛选依据",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue