[Lens] Performance refactoring for indexpattern fast lookup and Operation support matrix computation (#82829)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marco Liberati 2020-11-10 14:31:04 +01:00 committed by GitHub
parent 6003cadce4
commit 0b99841310
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 995 additions and 791 deletions

View file

@ -18,6 +18,131 @@ import { ChangeIndexPattern } from './change_indexpattern';
import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui';
import { documentField } from './document_field';
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
import { getFieldByNameFactory } from './pure_helpers';
const fieldsOne = [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'memory',
displayName: 'amemory',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'unsupported',
displayName: 'unsupported',
type: 'geo',
aggregatable: true,
searchable: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
},
{
name: 'client',
displayName: 'client',
type: 'ip',
aggregatable: true,
searchable: true,
},
documentField,
];
const fieldsTwo = [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
fixed_interval: '1d',
delay: '7d',
time_zone: 'UTC',
},
},
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
histogram: {
agg: 'histogram',
interval: 1000,
},
max: {
agg: 'max',
},
min: {
agg: 'min',
},
sum: {
agg: 'sum',
},
},
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
terms: {
agg: 'terms',
},
},
},
documentField,
];
const fieldsThree = [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
},
documentField,
];
const initialState: IndexPatternPrivateState = {
indexPatternRefs: [],
@ -85,139 +210,24 @@ const initialState: IndexPatternPrivateState = {
title: 'idx1',
timeFieldName: 'timestamp',
hasRestrictions: false,
fields: [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'memory',
displayName: 'amemory',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'unsupported',
displayName: 'unsupported',
type: 'geo',
aggregatable: true,
searchable: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
},
{
name: 'client',
displayName: 'client',
type: 'ip',
aggregatable: true,
searchable: true,
},
documentField,
],
fields: fieldsOne,
getFieldByName: getFieldByNameFactory(fieldsOne),
},
'2': {
id: '2',
title: 'idx2',
timeFieldName: 'timestamp',
hasRestrictions: true,
fields: [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
fixed_interval: '1d',
delay: '7d',
time_zone: 'UTC',
},
},
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
histogram: {
agg: 'histogram',
interval: 1000,
},
max: {
agg: 'max',
},
min: {
agg: 'min',
},
sum: {
agg: 'sum',
},
},
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
terms: {
agg: 'terms',
},
},
},
documentField,
],
fields: fieldsTwo,
getFieldByName: getFieldByNameFactory(fieldsTwo),
},
'3': {
id: '3',
title: 'idx3',
timeFieldName: 'timestamp',
hasRestrictions: false,
fields: [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
},
documentField,
],
fields: fieldsThree,
getFieldByName: getFieldByNameFactory(fieldsThree),
},
},
isFirstExistenceFetch: false,
@ -330,6 +340,7 @@ describe('IndexPattern Data Panel', () => {
title: 'aaa',
timeFieldName: 'atime',
fields: [],
getFieldByName: getFieldByNameFactory([]),
hasRestrictions: false,
},
b: {
@ -337,6 +348,7 @@ describe('IndexPattern Data Panel', () => {
title: 'bbb',
timeFieldName: 'btime',
fields: [],
getFieldByName: getFieldByNameFactory([]),
hasRestrictions: false,
},
},

View file

@ -5,7 +5,7 @@
*/
import './datapanel.scss';
import { uniq, keyBy, groupBy } from 'lodash';
import { uniq, groupBy } from 'lodash';
import React, { useState, memo, useCallback, useMemo } from 'react';
import {
EuiFlexGroup,
@ -266,9 +266,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
const fieldInfoUnavailable = existenceFetchFailed || currentIndexPattern.hasRestrictions;
const unfilteredFieldGroups: FieldGroups = useMemo(() => {
const fieldByName = keyBy(allFields, 'name');
const containsData = (field: IndexPatternField) => {
const overallField = fieldByName[field.name];
const overallField = currentIndexPattern.getFieldByName(field.name);
return (
overallField && fieldExists(existingFields, currentIndexPattern.title, overallField.name)

View file

@ -10,12 +10,14 @@ import { BucketNestingEditor } from './bucket_nesting_editor';
import { IndexPatternColumn } from '../indexpattern';
import { IndexPatternField } from '../types';
const fieldMap = {
const fieldMap: Record<string, IndexPatternField> = {
a: { displayName: 'a' } as IndexPatternField,
b: { displayName: 'b' } as IndexPatternField,
c: { displayName: 'c' } as IndexPatternField,
};
const getFieldByName = (name: string): IndexPatternField | undefined => fieldMap[name];
describe('BucketNestingEditor', () => {
function mockCol(col: Partial<IndexPatternColumn> = {}): IndexPatternColumn {
const result = {
@ -39,7 +41,7 @@ describe('BucketNestingEditor', () => {
it('should display the top level grouping when at the root', () => {
const component = mount(
<BucketNestingEditor
fieldMap={fieldMap}
getFieldByName={getFieldByName}
columnId="a"
layer={{
columnOrder: ['a', 'b', 'c'],
@ -61,7 +63,7 @@ describe('BucketNestingEditor', () => {
const component = mount(
<BucketNestingEditor
columnId="a"
fieldMap={fieldMap}
getFieldByName={getFieldByName}
layer={{
columnOrder: ['b', 'a', 'c'],
columns: {
@ -83,7 +85,7 @@ describe('BucketNestingEditor', () => {
const component = mount(
<BucketNestingEditor
columnId="a"
fieldMap={fieldMap}
getFieldByName={getFieldByName}
layer={{
columnOrder: ['b', 'a', 'c'],
columns: {
@ -128,7 +130,7 @@ describe('BucketNestingEditor', () => {
const component = mount(
<BucketNestingEditor
columnId="a"
fieldMap={fieldMap}
getFieldByName={getFieldByName}
layer={{
columnOrder: ['a', 'b', 'c'],
columns: {
@ -149,7 +151,7 @@ describe('BucketNestingEditor', () => {
const component = mount(
<BucketNestingEditor
columnId="a"
fieldMap={fieldMap}
getFieldByName={getFieldByName}
layer={{
columnOrder: ['a', 'b', 'c'],
columns: {
@ -170,7 +172,7 @@ describe('BucketNestingEditor', () => {
const component = mount(
<BucketNestingEditor
columnId="a"
fieldMap={fieldMap}
getFieldByName={getFieldByName}
layer={{
columnOrder: ['c', 'a', 'b'],
columns: {
@ -194,7 +196,7 @@ describe('BucketNestingEditor', () => {
const component = mount(
<BucketNestingEditor
columnId="a"
fieldMap={fieldMap}
getFieldByName={getFieldByName}
layer={{
columnOrder: ['c', 'a', 'b'],
columns: {
@ -221,7 +223,7 @@ describe('BucketNestingEditor', () => {
const component = mount(
<BucketNestingEditor
columnId="a"
fieldMap={fieldMap}
getFieldByName={getFieldByName}
layer={{
columnOrder: ['c', 'a', 'b'],
columns: {
@ -247,7 +249,7 @@ describe('BucketNestingEditor', () => {
const setColumns = jest.fn();
const component = mount(
<BucketNestingEditor
fieldMap={fieldMap}
getFieldByName={getFieldByName}
columnId="b"
layer={{
columnOrder: ['c', 'a', 'b'],

View file

@ -20,20 +20,25 @@ function nestColumn(columnOrder: string[], outer: string, inner: string) {
return result;
}
function getFieldName(fieldMap: Record<string, IndexPatternField>, column: IndexPatternColumn) {
return hasField(column) ? fieldMap[column.sourceField]?.displayName || column.sourceField : '';
function getFieldName(
column: IndexPatternColumn,
getFieldByName: (name: string) => IndexPatternField | undefined
) {
return hasField(column)
? getFieldByName(column.sourceField)?.displayName || column.sourceField
: '';
}
export function BucketNestingEditor({
columnId,
layer,
setColumns,
fieldMap,
getFieldByName,
}: {
columnId: string;
layer: IndexPatternLayer;
setColumns: (columns: string[]) => void;
fieldMap: Record<string, IndexPatternField>;
getFieldByName: (name: string) => IndexPatternField | undefined;
}) {
const column = layer.columns[columnId];
const columns = Object.entries(layer.columns);
@ -42,7 +47,7 @@ export function BucketNestingEditor({
.map(([value, c]) => ({
value,
text: c.label,
fieldName: getFieldName(fieldMap, c),
fieldName: getFieldName(c, getFieldByName),
operationType: c.operationType,
}));

View file

@ -29,7 +29,7 @@ import { deleteColumn, changeColumn, updateColumnParam, mergeLayer } from '../st
import { FieldSelect } from './field_select';
import { hasField, fieldIsInvalid } from '../utils';
import { BucketNestingEditor } from './bucket_nesting_editor';
import { IndexPattern, IndexPatternField } from '../types';
import { IndexPattern } from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
import { FormatSelector } from './format_selector';
@ -97,23 +97,13 @@ export function DimensionEditor(props: DimensionEditorProps) {
const ParamEditor = selectedOperationDefinition?.paramEditor;
const fieldMap: Record<string, IndexPatternField> = useMemo(() => {
const fields: Record<string, IndexPatternField> = {};
currentIndexPattern.fields.forEach((field) => {
fields[field.name] = field;
});
return fields;
}, [currentIndexPattern]);
const possibleOperations = useMemo(() => {
return Object.values(operationDefinitionMap)
.sort((op1, op2) => {
return op1.displayName.localeCompare(op2.displayName);
})
.map((def) => def.type)
.filter(
(type) => fieldByOperation[type]?.length || operationWithoutField.indexOf(type) !== -1
);
.filter((type) => fieldByOperation[type]?.size || operationWithoutField.has(type));
}, [fieldByOperation, operationWithoutField]);
// Operations are compatible if they match inputs. They are always compatible in
@ -128,7 +118,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
(selectedColumn &&
hasField(selectedColumn) &&
definition.input === 'field' &&
fieldByOperation[operationType]?.indexOf(selectedColumn.sourceField) !== -1) ||
fieldByOperation[operationType]?.has(selectedColumn.sourceField)) ||
(selectedColumn && !hasField(selectedColumn) && definition.input !== 'field'),
};
});
@ -198,9 +188,9 @@ export function DimensionEditor(props: DimensionEditorProps) {
trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
return;
} else if (!selectedColumn || !compatibleWithCurrentField) {
const possibleFields = fieldByOperation[operationType] || [];
const possibleFields = fieldByOperation[operationType] || new Set();
if (possibleFields.length === 1) {
if (possibleFields.size === 1) {
setState(
changeColumn({
state,
@ -212,7 +202,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
layerId: props.layerId,
op: operationType,
indexPattern: currentIndexPattern,
field: fieldMap[possibleFields[0]],
field: currentIndexPattern.getFieldByName(possibleFields.values().next().value),
previousColumn: selectedColumn,
}),
})
@ -236,7 +226,9 @@ export function DimensionEditor(props: DimensionEditorProps) {
layerId: props.layerId,
op: operationType,
indexPattern: currentIndexPattern,
field: hasField(selectedColumn) ? fieldMap[selectedColumn.sourceField] : undefined,
field: hasField(selectedColumn)
? currentIndexPattern.getFieldByName(selectedColumn.sourceField)
: undefined,
previousColumn: selectedColumn,
});
@ -297,7 +289,6 @@ export function DimensionEditor(props: DimensionEditorProps) {
fieldIsInvalid={currentFieldIsInvalid}
currentIndexPattern={currentIndexPattern}
existingFields={state.existingFields}
fieldMap={fieldMap}
operationSupportMatrix={operationSupportMatrix}
selectedColumnOperationType={selectedColumn && selectedColumn.operationType}
selectedColumnSourceField={
@ -323,25 +314,29 @@ export function DimensionEditor(props: DimensionEditorProps) {
) {
// If we just changed the field are not in an error state and the operation didn't change,
// we use the operations onFieldChange method to calculate the new column.
column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]);
column = changeField(
selectedColumn,
currentIndexPattern,
currentIndexPattern.getFieldByName(choice.field)!
);
} else {
// Otherwise we'll use the buildColumn method to calculate a new column
const compatibleOperations =
('field' in choice && operationSupportMatrix.operationByField[choice.field]) ||
[];
new Set();
let operation;
if (compatibleOperations.length > 0) {
if (compatibleOperations.size > 0) {
operation =
incompatibleSelectedOperationType &&
compatibleOperations.includes(incompatibleSelectedOperationType)
compatibleOperations.has(incompatibleSelectedOperationType)
? incompatibleSelectedOperationType
: compatibleOperations[0];
: compatibleOperations.values().next().value;
} else if ('field' in choice) {
operation = choice.operationType;
}
column = buildColumn({
columns: props.state.layers[props.layerId].columns,
field: fieldMap[choice.field],
field: currentIndexPattern.getFieldByName(choice.field),
indexPattern: currentIndexPattern,
layerId: props.layerId,
suggestedPriority: props.suggestedPriority,
@ -417,12 +412,12 @@ export function DimensionEditor(props: DimensionEditorProps) {
{!incompatibleSelectedOperationType && !hideGrouping && (
<BucketNestingEditor
fieldMap={fieldMap}
layer={state.layers[props.layerId]}
columnId={props.columnId}
setColumns={(columnOrder) =>
setState(mergeLayer({ state, layerId, newLayer: { columnOrder } }))
}
getFieldByName={currentIndexPattern.getFieldByName}
/>
)}

View file

@ -22,6 +22,7 @@ import { IndexPatternColumn } from '../operations';
import { documentField } from '../document_field';
import { OperationMetadata } from '../../types';
import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram';
import { getFieldByNameFactory } from '../pure_helpers';
jest.mock('../loader');
jest.mock('../state_helpers');
@ -34,6 +35,42 @@ jest.mock('lodash', () => {
};
});
const fields = [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
exists: true,
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
exists: true,
},
{
name: 'memory',
displayName: 'memory',
type: 'number',
aggregatable: true,
searchable: true,
exists: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
exists: true,
},
documentField,
];
const expectedIndexPatterns = {
1: {
id: '1',
@ -41,41 +78,8 @@ const expectedIndexPatterns = {
timeFieldName: 'timestamp',
hasExistence: true,
hasRestrictions: false,
fields: [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
exists: true,
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
exists: true,
},
{
name: 'memory',
displayName: 'memory',
type: 'number',
aggregatable: true,
searchable: true,
exists: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
exists: true,
},
documentField,
],
fields,
getFieldByName: getFieldByNameFactory(fields),
},
};

View file

@ -15,9 +15,46 @@ import { IndexPatternPrivateState } from '../types';
import { documentField } from '../document_field';
import { OperationMetadata } from '../../types';
import { IndexPatternColumn } from '../operations';
import { getFieldByNameFactory } from '../pure_helpers';
jest.mock('../state_helpers');
const fields = [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
exists: true,
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
exists: true,
},
{
name: 'memory',
displayName: 'memory',
type: 'number',
aggregatable: true,
searchable: true,
exists: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
exists: true,
},
documentField,
];
const expectedIndexPatterns = {
1: {
id: '1',
@ -25,41 +62,8 @@ const expectedIndexPatterns = {
timeFieldName: 'timestamp',
hasExistence: true,
hasRestrictions: false,
fields: [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
exists: true,
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
exists: true,
},
{
name: 'memory',
displayName: 'memory',
type: 'number',
aggregatable: true,
searchable: true,
exists: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
exists: true,
},
documentField,
],
fields,
getFieldByName: getFieldByNameFactory(fields),
},
};
@ -177,6 +181,23 @@ describe('IndexPatternDimensionEditorPanel', () => {
type: 'string',
},
],
getFieldByName: getFieldByNameFactory([
{
aggregatable: true,
name: 'bar',
displayName: 'bar',
searchable: true,
type: 'number',
},
{
aggregatable: true,
name: 'mystring',
displayName: 'mystring',
searchable: true,
type: 'string',
},
]),
},
},
currentIndexPatternId: '1',

View file

@ -137,9 +137,9 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPr
hasField(selectedColumn) &&
selectedColumn.sourceField !== droppedItem.field.name &&
operationsForNewField &&
operationsForNewField.includes(selectedColumn.operationType);
operationsForNewField.has(selectedColumn.operationType);
if (!operationsForNewField || operationsForNewField.length === 0) {
if (!operationsForNewField || operationsForNewField.size === 0) {
return false;
}
@ -148,7 +148,7 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPr
const newColumn = hasFieldChanged
? changeField(selectedColumn, currentIndexPattern, droppedItem.field)
: buildColumn({
op: operationsForNewField[0],
op: operationsForNewField.values().next().value,
columns: state.layers[layerId].columns,
indexPattern: currentIndexPattern,
layerId,

View file

@ -21,7 +21,7 @@ import { OperationType } from '../indexpattern';
import { LensFieldIcon } from '../lens_field_icon';
import { DataType } from '../../types';
import { OperationSupportMatrix } from './operation_support';
import { IndexPattern, IndexPatternField, IndexPatternPrivateState } from '../types';
import { IndexPattern, IndexPatternPrivateState } from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
import { fieldExists } from '../pure_helpers';
@ -33,7 +33,6 @@ export interface FieldChoice {
export interface FieldSelectProps extends EuiComboBoxProps<{}> {
currentIndexPattern: IndexPattern;
fieldMap: Record<string, IndexPatternField>;
incompatibleSelectedOperationType: OperationType | null;
selectedColumnOperationType?: OperationType;
selectedColumnSourceField?: string;
@ -46,7 +45,6 @@ export interface FieldSelectProps extends EuiComboBoxProps<{}> {
export function FieldSelect({
currentIndexPattern,
fieldMap,
incompatibleSelectedOperationType,
selectedColumnOperationType,
selectedColumnSourceField,
@ -63,31 +61,32 @@ export function FieldSelect({
function isCompatibleWithCurrentOperation(fieldName: string) {
if (incompatibleSelectedOperationType) {
return operationByField[fieldName]!.includes(incompatibleSelectedOperationType);
return operationByField[fieldName]!.has(incompatibleSelectedOperationType);
}
return (
!selectedColumnOperationType ||
operationByField[fieldName]!.includes(selectedColumnOperationType)
operationByField[fieldName]!.has(selectedColumnOperationType)
);
}
const [specialFields, normalFields] = _.partition(
fields,
(field) => fieldMap[field].type === 'document'
(field) => currentIndexPattern.getFieldByName(field)?.type === 'document'
);
const containsData = (field: string) =>
fieldMap[field].type === 'document' ||
currentIndexPattern.getFieldByName(field)?.type === 'document' ||
fieldExists(existingFields, currentIndexPattern.title, field);
function fieldNamesToOptions(items: string[]) {
return items
.filter((field) => currentIndexPattern.getFieldByName(field)?.displayName)
.map((field) => ({
label: fieldMap[field].displayName,
label: currentIndexPattern.getFieldByName(field)?.displayName,
value: {
type: 'field',
field,
dataType: fieldMap[field].type,
dataType: currentIndexPattern.getFieldByName(field)?.type,
operationType:
selectedColumnOperationType && isCompatibleWithCurrentOperation(field)
? selectedColumnOperationType
@ -118,7 +117,10 @@ export function FieldSelect({
}));
}
const [metaFields, nonMetaFields] = _.partition(normalFields, (field) => fieldMap[field].meta);
const [metaFields, nonMetaFields] = _.partition(
normalFields,
(field) => currentIndexPattern.getFieldByName(field)?.meta
);
const [availableFields, emptyFields] = _.partition(nonMetaFields, containsData);
const constructFieldsOptions = (fieldsArr: string[], label: string) =>
@ -158,7 +160,6 @@ export function FieldSelect({
incompatibleSelectedOperationType,
selectedColumnOperationType,
currentIndexPattern,
fieldMap,
operationByField,
existingFields,
]);
@ -180,7 +181,7 @@ export function FieldSelect({
{
label: fieldIsInvalid
? selectedColumnSourceField
: fieldMap[selectedColumnSourceField]?.displayName,
: currentIndexPattern.getFieldByName(selectedColumnSourceField)?.displayName,
value: { type: 'field', field: selectedColumnSourceField },
},
]

View file

@ -11,9 +11,9 @@ import { getAvailableOperationsByMetadata } from '../operations';
import { IndexPatternPrivateState } from '../types';
export interface OperationSupportMatrix {
operationByField: Partial<Record<string, OperationType[]>>;
operationWithoutField: OperationType[];
fieldByOperation: Partial<Record<OperationType, string[]>>;
operationByField: Partial<Record<string, Set<OperationType>>>;
operationWithoutField: Set<OperationType>;
fieldByOperation: Partial<Record<OperationType, Set<string>>>;
}
type Props = Pick<
@ -31,30 +31,30 @@ export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix
currentIndexPattern
).filter((operation) => props.filterOperations(operation.operationMetaData));
const supportedOperationsByField: Partial<Record<string, OperationType[]>> = {};
const supportedOperationsWithoutField: OperationType[] = [];
const supportedFieldsByOperation: Partial<Record<OperationType, string[]>> = {};
const supportedOperationsByField: Partial<Record<string, Set<OperationType>>> = {};
const supportedOperationsWithoutField: Set<OperationType> = new Set();
const supportedFieldsByOperation: Partial<Record<OperationType, Set<string>>> = {};
filteredOperationsByMetadata.forEach(({ operations }) => {
operations.forEach((operation) => {
if (operation.type === 'field') {
supportedOperationsByField[operation.field] = [
...(supportedOperationsByField[operation.field] ?? []),
operation.operationType,
];
if (!supportedOperationsByField[operation.field]) {
supportedOperationsByField[operation.field] = new Set();
}
supportedOperationsByField[operation.field]?.add(operation.operationType);
supportedFieldsByOperation[operation.operationType] = [
...(supportedFieldsByOperation[operation.operationType] ?? []),
operation.field,
];
if (!supportedFieldsByOperation[operation.operationType]) {
supportedFieldsByOperation[operation.operationType] = new Set();
}
supportedFieldsByOperation[operation.operationType]?.add(operation.field);
} else if (operation.type === 'none') {
supportedOperationsWithoutField.push(operation.operationType);
supportedOperationsWithoutField.add(operation.operationType);
}
});
});
return {
operationByField: _.mapValues(supportedOperationsByField, _.uniq),
operationWithoutField: _.uniq(supportedOperationsWithoutField),
fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq),
operationByField: supportedOperationsByField,
operationWithoutField: supportedOperationsWithoutField,
fieldByOperation: supportedFieldsByOperation,
};
};

View file

@ -12,121 +12,128 @@ import { IndexPatternPersistedState, IndexPatternPrivateState } from './types';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { Ast } from '@kbn/interpreter/common';
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
import { getFieldByNameFactory } from './pure_helpers';
jest.mock('./loader');
jest.mock('../id_generator');
const fieldsOne = [
{
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,
},
{
name: 'dest',
displayName: 'dest',
type: 'string',
aggregatable: true,
searchable: true,
},
];
const fieldsTwo = [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
fixed_interval: '1d',
delay: '7d',
time_zone: 'UTC',
},
},
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
// Ignored in the UI
histogram: {
agg: 'histogram',
interval: 1000,
},
avg: {
agg: 'avg',
},
max: {
agg: 'max',
},
min: {
agg: 'min',
},
sum: {
agg: 'sum',
},
},
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
terms: {
agg: 'terms',
},
},
},
];
const expectedIndexPatterns = {
1: {
id: '1',
title: 'my-fake-index-pattern',
timeFieldName: 'timestamp',
hasRestrictions: false,
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,
},
{
name: 'dest',
displayName: 'dest',
type: 'string',
aggregatable: true,
searchable: true,
},
],
fields: fieldsOne,
getFieldByName: getFieldByNameFactory(fieldsOne),
},
2: {
id: '2',
title: 'my-fake-restricted-pattern',
timeFieldName: 'timestamp',
hasRestrictions: true,
fields: [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
fixed_interval: '1d',
delay: '7d',
time_zone: 'UTC',
},
},
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
// Ignored in the UI
histogram: {
agg: 'histogram',
interval: 1000,
},
avg: {
agg: 'avg',
},
max: {
agg: 'max',
},
min: {
agg: 'min',
},
sum: {
agg: 'sum',
},
},
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
terms: {
agg: 'terms',
},
},
},
],
fields: fieldsTwo,
getFieldByName: getFieldByNameFactory(fieldsTwo),
},
};

View file

@ -12,121 +12,128 @@ import {
getDatasourceSuggestionsFromCurrentState,
getDatasourceSuggestionsForVisualizeField,
} from './indexpattern_suggestions';
import { getFieldByNameFactory } from './pure_helpers';
jest.mock('./loader');
jest.mock('../id_generator');
const fieldsOne = [
{
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,
},
{
name: 'dest',
displayName: 'dest',
type: 'string',
aggregatable: true,
searchable: true,
},
];
const fieldsTwo = [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
fixed_interval: '1d',
delay: '7d',
time_zone: 'UTC',
},
},
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
// Ignored in the UI
histogram: {
agg: 'histogram',
interval: 1000,
},
avg: {
agg: 'avg',
},
max: {
agg: 'max',
},
min: {
agg: 'min',
},
sum: {
agg: 'sum',
},
},
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
terms: {
agg: 'terms',
},
},
},
];
const expectedIndexPatterns = {
1: {
id: '1',
title: 'my-fake-index-pattern',
timeFieldName: 'timestamp',
hasRestrictions: false,
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,
},
{
name: 'dest',
displayName: 'dest',
type: 'string',
aggregatable: true,
searchable: true,
},
],
fields: fieldsOne,
getFieldByName: getFieldByNameFactory(fieldsOne),
},
2: {
id: '2',
title: 'my-fake-restricted-pattern',
hasRestrictions: true,
timeFieldName: 'timestamp',
fields: [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
fixed_interval: '1d',
delay: '7d',
time_zone: 'UTC',
},
},
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
// Ignored in the UI
histogram: {
agg: 'histogram',
interval: 1000,
},
avg: {
agg: 'avg',
},
max: {
agg: 'max',
},
min: {
agg: 'min',
},
sum: {
agg: 'sum',
},
},
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
terms: {
agg: 'terms',
},
},
},
],
fields: fieldsTwo,
getFieldByName: getFieldByNameFactory(fieldsTwo),
},
};
@ -335,6 +342,15 @@ describe('IndexPattern Data Source suggestions', () => {
searchable: true,
},
],
getFieldByName: getFieldByNameFactory([
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
]),
},
},
layers: {
@ -546,6 +562,16 @@ describe('IndexPattern Data Source suggestions', () => {
searchable: true,
},
],
getFieldByName: getFieldByNameFactory([
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
]),
},
},
layers: {
@ -1531,6 +1557,43 @@ describe('IndexPattern Data Source suggestions', () => {
it('returns simplified versions of table with more than 2 columns', () => {
const initialState = testInitialState();
const fields = [
{
name: 'field1',
displayName: 'field1',
type: 'string',
aggregatable: true,
searchable: true,
},
{
name: 'field2',
displayName: 'field2',
type: 'string',
aggregatable: true,
searchable: true,
},
{
name: 'field3',
displayName: 'field3Label',
type: 'string',
aggregatable: true,
searchable: true,
},
{
name: 'field4',
displayName: 'field4',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'field5',
displayName: 'field5',
type: 'number',
aggregatable: true,
searchable: true,
},
];
const state: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
@ -1540,43 +1603,8 @@ describe('IndexPattern Data Source suggestions', () => {
id: '1',
title: 'my-fake-index-pattern',
hasRestrictions: false,
fields: [
{
name: 'field1',
displayName: 'field1',
type: 'string',
aggregatable: true,
searchable: true,
},
{
name: 'field2',
displayName: 'field2',
type: 'string',
aggregatable: true,
searchable: true,
},
{
name: 'field3',
displayName: 'field3Label',
type: 'string',
aggregatable: true,
searchable: true,
},
{
name: 'field4',
displayName: 'field4',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'field5',
displayName: 'field5',
type: 'number',
aggregatable: true,
searchable: true,
},
],
fields,
getFieldByName: getFieldByNameFactory(fields),
},
},
isFirstExistenceFetch: false,
@ -1700,6 +1728,23 @@ describe('IndexPattern Data Source suggestions', () => {
searchable: true,
},
],
getFieldByName: getFieldByNameFactory([
{
name: 'field1',
displayName: 'field1',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'field2',
displayName: 'field2',
type: 'date',
aggregatable: true,
searchable: true,
},
]),
},
},
isFirstExistenceFetch: false,
@ -1756,6 +1801,15 @@ describe('IndexPattern Data Source suggestions', () => {
searchable: true,
},
],
getFieldByName: getFieldByNameFactory([
{
name: 'field1',
displayName: 'field1',
type: 'number',
aggregatable: true,
searchable: true,
},
]),
},
},
isFirstExistenceFetch: false,

View file

@ -128,7 +128,7 @@ export function getDatasourceSuggestionsForVisualizeField(
const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId);
// Identify the field by the indexPatternId and the fieldName
const indexPattern = state.indexPatterns[indexPatternId];
const field = indexPattern.fields.find((fld) => fld.name === fieldName);
const field = indexPattern.getFieldByName(fieldName);
if (layerIds.length !== 0 || !field) return [];
const newId = generateId();
@ -371,7 +371,7 @@ function createNewLayerWithMetricAggregation(
indexPattern: IndexPattern,
field: IndexPatternField
): IndexPatternLayer {
const dateField = indexPattern.fields.find((f) => f.name === indexPattern.timeFieldName)!;
const dateField = indexPattern.getFieldByName(indexPattern.timeFieldName!);
const column = getMetricColumn(indexPattern, layerId, field);
@ -451,9 +451,8 @@ export function getDatasourceSuggestionsFromCurrentState(
(columnId) =>
layer.columns[columnId].isBucketed && layer.columns[columnId].dataType === 'date'
);
const timeField = indexPattern.fields.find(
({ name }) => name === indexPattern.timeFieldName
);
const timeField =
indexPattern.timeFieldName && indexPattern.getFieldByName(indexPattern.timeFieldName);
const hasNumericDimension =
buckets.length === 1 &&
@ -507,17 +506,17 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId
const layer = state.layers[layerId];
const [firstBucket, secondBucket, ...rest] = layer.columnOrder;
const updatedLayer = { ...layer, columnOrder: [secondBucket, firstBucket, ...rest] };
const currentFields = state.indexPatterns[state.currentIndexPatternId].fields;
const indexPattern = state.indexPatterns[state.currentIndexPatternId];
const firstBucketColumn = layer.columns[firstBucket];
const firstBucketLabel =
currentFields.find((field) => {
const column = layer.columns[firstBucket];
return hasField(column) && column.sourceField === field.name;
})?.displayName || '';
(hasField(firstBucketColumn) &&
indexPattern.getFieldByName(firstBucketColumn.sourceField)?.displayName) ||
'';
const secondBucketColumn = layer.columns[secondBucket];
const secondBucketLabel =
currentFields.find((field) => {
const column = layer.columns[secondBucket];
return hasField(column) && column.sourceField === field.name;
})?.displayName || '';
(hasField(secondBucketColumn) &&
indexPattern.getFieldByName(secondBucketColumn.sourceField)?.displayName) ||
'';
return buildSuggestion({
state,
@ -604,7 +603,10 @@ function createAlternativeMetricSuggestions(
if (!hasField(column)) {
return;
}
const field = indexPattern.fields.find(({ name }) => column.sourceField === name)!;
const field = indexPattern.getFieldByName(column.sourceField);
if (!field) {
return;
}
const alternativeMetricOperations = getOperationTypesForField(field)
.map((op) =>
buildColumn({

View file

@ -11,6 +11,7 @@ import { shallowWithIntl as shallow } from 'test_utils/enzyme_helpers';
import { ShallowWrapper } from 'enzyme';
import { EuiSelectable } from '@elastic/eui';
import { ChangeIndexPattern } from './change_indexpattern';
import { getFieldByNameFactory } from './pure_helpers';
jest.mock('./state_helpers');
@ -19,6 +20,120 @@ interface IndexPatternPickerOption {
checked?: 'on' | 'off';
}
const fieldsOne = [
{
name: 'timestamp',
displayName: 'timestampLabel',
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: 'unsupported',
displayName: 'unsupported',
type: 'geo',
aggregatable: true,
searchable: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
},
];
const fieldsTwo = [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
fixed_interval: '1d',
delay: '7d',
time_zone: 'UTC',
},
},
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
histogram: {
agg: 'histogram',
interval: 1000,
},
max: {
agg: 'max',
},
min: {
agg: 'min',
},
sum: {
agg: 'sum',
},
},
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
terms: {
agg: 'terms',
},
},
},
];
const fieldsThree = [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
},
{
name: 'memory',
displayName: 'memory',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
},
];
const initialState: IndexPatternPrivateState = {
indexPatternRefs: [
{ id: '1', title: 'my-fake-index-pattern' },
@ -63,129 +178,24 @@ const initialState: IndexPatternPrivateState = {
title: 'my-fake-index-pattern',
timeFieldName: 'timestamp',
hasRestrictions: false,
fields: [
{
name: 'timestamp',
displayName: 'timestampLabel',
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: 'unsupported',
displayName: 'unsupported',
type: 'geo',
aggregatable: true,
searchable: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
},
],
fields: fieldsOne,
getFieldByName: getFieldByNameFactory(fieldsOne),
},
'2': {
id: '2',
title: 'my-fake-restricted-pattern',
hasRestrictions: true,
timeFieldName: 'timestamp',
fields: [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
fixed_interval: '1d',
delay: '7d',
time_zone: 'UTC',
},
},
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
histogram: {
agg: 'histogram',
interval: 1000,
},
max: {
agg: 'max',
},
min: {
agg: 'min',
},
sum: {
agg: 'sum',
},
},
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
terms: {
agg: 'terms',
},
},
},
],
fields: fieldsTwo,
getFieldByName: getFieldByNameFactory(fieldsTwo),
},
'3': {
id: '3',
title: 'my-compatible-pattern',
timeFieldName: 'timestamp',
hasRestrictions: false,
fields: [
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
},
{
name: 'memory',
displayName: 'memory',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
},
],
fields: fieldsThree,
getFieldByName: getFieldByNameFactory(fieldsThree),
},
},
};

View file

@ -285,15 +285,10 @@ describe('loader', () => {
} as unknown) as Pick<IndexPatternsContract, 'get'>,
});
expect(
cache.foo.fields.find((f: IndexPatternField) => f.name === 'bytes')!.aggregationRestrictions
).toEqual({
expect(cache.foo.getFieldByName('bytes')!.aggregationRestrictions).toEqual({
sum: { agg: 'sum' },
});
expect(
cache.foo.fields.find((f: IndexPatternField) => f.name === 'timestamp')!
.aggregationRestrictions
).toEqual({
expect(cache.foo.getFieldByName('timestamp')!.aggregationRestrictions).toEqual({
date_histogram: { agg: 'date_histogram', fixed_interval: 'm' },
});
});
@ -342,9 +337,7 @@ describe('loader', () => {
} as unknown) as Pick<IndexPatternsContract, 'get'>,
});
expect(cache.foo.fields.find((f: IndexPatternField) => f.name === 'timestamp')!.meta).toEqual(
true
);
expect(cache.foo.getFieldByName('timestamp')!.meta).toEqual(true);
});
});

View file

@ -26,6 +26,7 @@ import {
import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/public';
import { documentField } from './document_field';
import { readFromStorage, writeToStorage } from '../settings_storage';
import { getFieldByNameFactory } from './pure_helpers';
type SetState = StateSetter<IndexPatternPrivateState>;
type SavedObjectsClient = Pick<SavedObjectsClientContract, 'find'>;
@ -112,6 +113,7 @@ export async function loadIndexPatterns({
])
),
fields: newFields,
getFieldByName: getFieldByNameFactory(newFields),
hasRestrictions: !!typeMeta?.aggs,
};

View file

@ -5,14 +5,11 @@
*/
import { DragContextState } from '../drag_drop';
import { getFieldByNameFactory } from './pure_helpers';
import { IndexPattern } from './types';
export const createMockedIndexPattern = (): IndexPattern => ({
id: '1',
title: 'my-fake-index-pattern',
timeFieldName: 'timestamp',
hasRestrictions: false,
fields: [
export const createMockedIndexPattern = (): IndexPattern => {
const fields = [
{
name: 'timestamp',
displayName: 'timestampLabel',
@ -74,16 +71,19 @@ export const createMockedIndexPattern = (): IndexPattern => ({
lang: 'painless',
script: '1234',
},
],
});
];
return {
id: '1',
title: 'my-fake-index-pattern',
timeFieldName: 'timestamp',
hasRestrictions: false,
fields,
getFieldByName: getFieldByNameFactory(fields),
};
};
export const createMockedRestrictedIndexPattern = () => ({
id: '2',
title: 'my-fake-restricted-pattern',
timeFieldName: 'timestamp',
hasRestrictions: true,
fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } },
fields: [
export const createMockedRestrictedIndexPattern = () => {
const fields = [
{
name: 'timestamp',
displayName: 'timestampLabel',
@ -109,54 +109,63 @@ export const createMockedRestrictedIndexPattern = () => ({
lang: 'painless',
script: '1234',
},
],
typeMeta: {
params: {
rollup_index: 'my-fake-index-pattern',
},
aggs: {
terms: {
source: {
agg: 'terms',
},
];
return {
id: '2',
title: 'my-fake-restricted-pattern',
timeFieldName: 'timestamp',
hasRestrictions: true,
fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } },
fields,
getFieldByName: getFieldByNameFactory(fields),
typeMeta: {
params: {
rollup_index: 'my-fake-index-pattern',
},
date_histogram: {
timestamp: {
agg: 'date_histogram',
fixed_interval: '1d',
delay: '7d',
time_zone: 'UTC',
aggs: {
terms: {
source: {
agg: 'terms',
},
},
},
histogram: {
bytes: {
agg: 'histogram',
interval: 1000,
date_histogram: {
timestamp: {
agg: 'date_histogram',
fixed_interval: '1d',
delay: '7d',
time_zone: 'UTC',
},
},
},
avg: {
bytes: {
agg: 'avg',
histogram: {
bytes: {
agg: 'histogram',
interval: 1000,
},
},
},
max: {
bytes: {
agg: 'max',
avg: {
bytes: {
agg: 'avg',
},
},
},
min: {
bytes: {
agg: 'min',
max: {
bytes: {
agg: 'max',
},
},
},
sum: {
bytes: {
agg: 'sum',
min: {
bytes: {
agg: 'min',
},
},
sum: {
bytes: {
agg: 'sum',
},
},
},
},
},
});
};
};
export function createMockedDragDropContext(): jest.Mocked<DragContextState> {
return {

View file

@ -43,7 +43,7 @@ export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternCo
}
},
isTransferable: (column, newIndexPattern) => {
const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField);
const newField = newIndexPattern.getFieldByName(column.sourceField);
return Boolean(
newField &&

View file

@ -18,6 +18,7 @@ import {
} from '../../../../../../../src/plugins/data/public/mocks';
import { createMockedIndexPattern } from '../../mocks';
import { IndexPatternPrivateState } from '../../types';
import { getFieldByNameFactory } from '../../pure_helpers';
const dataStart = dataPluginMock.createStartContract();
dataStart.search.aggs.calculateAutoTimeExpression = getCalculateAutoTimeExpression(
@ -66,6 +67,17 @@ describe('date_histogram', () => {
searchable: true,
},
],
getFieldByName: getFieldByNameFactory([
{
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
esTypes: ['date'],
aggregatable: true,
searchable: true,
},
]),
},
2: {
id: '2',
@ -81,6 +93,16 @@ describe('date_histogram', () => {
searchable: true,
},
],
getFieldByName: getFieldByNameFactory([
{
name: 'other_timestamp',
displayName: 'other_timestamp',
type: 'date',
esTypes: ['date'],
aggregatable: true,
searchable: true,
},
]),
},
},
layers: {
@ -267,6 +289,22 @@ describe('date_histogram', () => {
},
},
],
getFieldByName: getFieldByNameFactory([
{
name: 'timestamp',
displayName: 'timestamp',
aggregatable: true,
searchable: true,
type: 'date',
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
time_zone: 'UTC',
calendar_interval: '42w',
},
},
},
]),
}
);
expect(esAggsConfig).toEqual(
@ -294,7 +332,7 @@ describe('date_histogram', () => {
},
};
const indexPattern = createMockedIndexPattern();
const newDateField = indexPattern.fields.find((i) => i.name === 'start_date')!;
const newDateField = indexPattern.getFieldByName('start_date')!;
const column = dateHistogramOperation.onFieldChange(oldColumn, indexPattern, newDateField);
expect(column).toHaveProperty('sourceField', 'start_date');
@ -314,7 +352,7 @@ describe('date_histogram', () => {
},
};
const indexPattern = createMockedIndexPattern();
const newDateField = indexPattern.fields.find((i) => i.name === 'start_date')!;
const newDateField = indexPattern.getFieldByName('start_date')!;
const column = dateHistogramOperation.onFieldChange(oldColumn, indexPattern, newDateField);
expect(column).toHaveProperty('sourceField', 'start_date');
@ -356,6 +394,22 @@ describe('date_histogram', () => {
},
},
],
getFieldByName: getFieldByNameFactory([
{
name: 'dateField',
displayName: 'dateField',
type: 'date',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
time_zone: 'CET',
calendar_interval: 'w',
},
},
},
]),
}
);
expect(transferedColumn).toEqual(
@ -393,6 +447,15 @@ describe('date_histogram', () => {
searchable: true,
},
],
getFieldByName: getFieldByNameFactory([
{
name: 'dateField',
displayName: 'dateField',
type: 'date',
aggregatable: true,
searchable: true,
},
]),
}
);
expect(transferedColumn).toEqual(
@ -609,6 +672,18 @@ describe('date_histogram', () => {
},
},
],
getFieldByName: getFieldByNameFactory([
{
...state.indexPatterns[1].fields[0],
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
time_zone: 'UTC',
calendar_interval: '1h',
},
},
},
]),
},
},
}}

View file

@ -81,7 +81,7 @@ export const dateHistogramOperation: OperationDefinition<
};
},
isTransferable: (column, newIndexPattern) => {
const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField);
const newField = newIndexPattern.getFieldByName(column.sourceField);
return Boolean(
newField &&
@ -91,12 +91,9 @@ export const dateHistogramOperation: OperationDefinition<
);
},
transfer: (column, newIndexPattern) => {
const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField);
if (
newField &&
newField.aggregationRestrictions &&
newField.aggregationRestrictions.date_histogram
) {
const newField = newIndexPattern.getFieldByName(column.sourceField);
if (newField?.aggregationRestrictions?.date_histogram) {
const restrictions = newField.aggregationRestrictions.date_histogram;
return {
@ -123,7 +120,7 @@ export const dateHistogramOperation: OperationDefinition<
};
},
toEsAggsConfig: (column, columnId, indexPattern) => {
const usedField = indexPattern.fields.find((field) => field.name === column.sourceField);
const usedField = indexPattern.getFieldByName(column.sourceField);
return {
id: columnId,
enabled: true,
@ -132,7 +129,7 @@ export const dateHistogramOperation: OperationDefinition<
params: {
field: column.sourceField,
time_zone: column.params.timeZone,
useNormalizedEsInterval: !usedField || !usedField.aggregationRestrictions?.date_histogram,
useNormalizedEsInterval: !usedField?.aggregationRestrictions?.date_histogram,
interval: column.params.interval,
drop_partials: false,
min_doc_count: 0,
@ -143,8 +140,8 @@ export const dateHistogramOperation: OperationDefinition<
paramEditor: ({ state, setState, currentColumn, layerId, dateRange, data }) => {
const field =
currentColumn &&
state.indexPatterns[state.layers[layerId].indexPatternId].fields.find(
(currentField) => currentField.name === currentColumn.sourceField
state.indexPatterns[state.layers[layerId].indexPatternId].getFieldByName(
currentColumn.sourceField
);
const intervalIsRestricted =
field!.aggregationRestrictions && field!.aggregationRestrictions.date_histogram;

View file

@ -43,7 +43,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
}
},
isTransferable: (column, newIndexPattern) => {
const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField);
const newField = newIndexPattern.getFieldByName(column.sourceField);
return Boolean(
newField &&

View file

@ -30,6 +30,7 @@ import {
} from './constants';
import { RangePopover } from './advanced_editor';
import { DragDropBuckets } from '../shared_components';
import { getFieldByNameFactory } from '../../../pure_helpers';
const dataPluginMockValue = dataPluginMock.createStartContract();
// need to overwrite the formatter field first
@ -96,6 +97,9 @@ describe('ranges', () => {
title: 'my_index_pattern',
hasRestrictions: false,
fields: [{ name: sourceField, type: 'number', displayName: sourceField }],
getFieldByName: getFieldByNameFactory([
{ name: sourceField, type: 'number', displayName: sourceField },
]),
},
},
existingFields: {},

View file

@ -140,7 +140,7 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field
};
},
isTransferable: (column, newIndexPattern) => {
const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField);
const newField = newIndexPattern.getFieldByName(column.sourceField);
return Boolean(
newField &&
@ -168,9 +168,7 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field
},
paramEditor: ({ state, setState, currentColumn, layerId, columnId, uiSettings, data }) => {
const indexPattern = state.indexPatterns[state.layers[layerId].indexPatternId];
const currentField = indexPattern.fields.find(
(field) => field.name === currentColumn.sourceField
);
const currentField = indexPattern.getFieldByName(currentColumn.sourceField);
const numberFormat = currentColumn.params.format;
const numberFormatterPattern =
numberFormat &&

View file

@ -61,7 +61,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
}
},
isTransferable: (column, newIndexPattern) => {
const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField);
const newField = newIndexPattern.getFieldByName(column.sourceField);
return Boolean(
newField &&

View file

@ -103,7 +103,7 @@ describe('terms', () => {
},
};
const indexPattern = createMockedIndexPattern();
const newNumberField = indexPattern.fields.find((i) => i.name === 'bytes')!;
const newNumberField = indexPattern.getFieldByName('bytes')!;
const column = termsOperation.onFieldChange(oldColumn, indexPattern, newNumberField);
expect(column).toHaveProperty('dataType', 'number');

View file

@ -8,38 +8,42 @@ import { getOperationTypesForField, getAvailableOperationsByMetadata, buildColum
import { AvgIndexPatternColumn } from './definitions/metrics';
import { IndexPatternPrivateState } from '../types';
import { documentField } from '../document_field';
import { getFieldByNameFactory } from '../pure_helpers';
jest.mock('../loader');
const fields = [
{
name: 'timestamp',
displayName: 'timestamp',
type: 'date',
aggregatable: true,
searchable: true,
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
},
];
const expectedIndexPatterns = {
1: {
id: '1',
title: 'my-fake-index-pattern',
timeFieldName: 'timestamp',
hasRestrictions: false,
fields: [
{
name: 'timestamp',
displayName: 'timestamp',
type: 'date',
aggregatable: true,
searchable: true,
},
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
},
],
fields,
getFieldByName: getFieldByNameFactory(fields),
},
};

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { IndexPatternPrivateState } from './types';
import { keyBy } from 'lodash';
import { IndexPatternField, IndexPatternPrivateState } from './types';
export function fieldExists(
existingFields: IndexPatternPrivateState['existingFields'],
@ -13,3 +14,8 @@ export function fieldExists(
) {
return existingFields[indexPatternTitle] && existingFields[indexPatternTitle][fieldName];
}
export function getFieldByNameFactory(newFields: IndexPatternField[]) {
const fieldsLookup = keyBy(newFields, 'name');
return (name: string) => fieldsLookup[name];
}

View file

@ -16,6 +16,7 @@ import { TermsIndexPatternColumn } from './operations/definitions/terms';
import { DateHistogramIndexPatternColumn } from './operations/definitions/date_histogram';
import { AvgIndexPatternColumn } from './operations/definitions/metrics';
import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types';
import { getFieldByNameFactory } from './pure_helpers';
jest.mock('./operations');
@ -585,59 +586,61 @@ describe('state_helpers', () => {
});
describe('updateLayerIndexPattern', () => {
const fields = [
{
name: 'fieldA',
displayName: 'fieldA',
aggregatable: true,
searchable: true,
type: 'string',
},
{
name: 'fieldB',
displayName: 'fieldB',
aggregatable: true,
searchable: true,
type: 'number',
aggregationRestrictions: {
avg: {
agg: 'avg',
},
},
},
{
name: 'fieldC',
displayName: 'fieldC',
aggregatable: false,
searchable: true,
type: 'date',
},
{
name: 'fieldD',
displayName: 'fieldD',
aggregatable: true,
searchable: true,
type: 'date',
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
time_zone: 'CET',
calendar_interval: 'w',
},
},
},
{
name: 'fieldE',
displayName: 'fieldE',
aggregatable: true,
searchable: true,
type: 'date',
},
];
const indexPattern: IndexPattern = {
id: 'test',
title: '',
hasRestrictions: true,
fields: [
{
name: 'fieldA',
displayName: 'fieldA',
aggregatable: true,
searchable: true,
type: 'string',
},
{
name: 'fieldB',
displayName: 'fieldB',
aggregatable: true,
searchable: true,
type: 'number',
aggregationRestrictions: {
avg: {
agg: 'avg',
},
},
},
{
name: 'fieldC',
displayName: 'fieldC',
aggregatable: false,
searchable: true,
type: 'date',
},
{
name: 'fieldD',
displayName: 'fieldD',
aggregatable: true,
searchable: true,
type: 'date',
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
time_zone: 'CET',
calendar_interval: 'w',
},
},
},
{
name: 'fieldE',
displayName: 'fieldE',
aggregatable: true,
searchable: true,
type: 'date',
},
],
getFieldByName: getFieldByNameFactory(fields),
fields,
};
it('should switch index pattern id in layer', () => {

View file

@ -11,6 +11,7 @@ import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/pub
export interface IndexPattern {
id: string;
fields: IndexPatternField[];
getFieldByName(name: string): IndexPatternField | undefined;
title: string;
timeFieldName?: string;
fieldFormatMap?: Record<

View file

@ -87,15 +87,15 @@ export function fieldIsInvalid(
indexPattern: IndexPattern
) {
const operationDefinition = operationType && operationDefinitionMap[operationType];
const field = sourceField ? indexPattern.getFieldByName(sourceField) : undefined;
return Boolean(
sourceField &&
operationDefinition &&
!indexPattern.fields.some(
(field) =>
field.name === sourceField &&
operationDefinition?.input === 'field' &&
operationDefinition.getPossibleOperationForField(field) !== undefined
!(
field &&
operationDefinition?.input === 'field' &&
operationDefinition.getPossibleOperationForField(field) !== undefined
)
);
}