kibana/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
2020-11-17 10:11:44 +01:00

1965 lines
57 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { DatasourceSuggestion } from '../types';
import { generateId } from '../id_generator';
import { IndexPatternPrivateState } from './types';
import {
getDatasourceSuggestionsForField,
getDatasourceSuggestionsFromCurrentState,
getDatasourceSuggestionsForVisualizeField,
} from './indexpattern_suggestions';
import { documentField } from './document_field';
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,
},
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: {
// 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',
},
},
},
documentField,
];
const expectedIndexPatterns = {
1: {
id: '1',
title: 'my-fake-index-pattern',
timeFieldName: 'timestamp',
hasRestrictions: false,
fields: fieldsOne,
getFieldByName: getFieldByNameFactory(fieldsOne),
},
2: {
id: '2',
title: 'my-fake-restricted-pattern',
hasRestrictions: true,
timeFieldName: 'timestamp',
fields: fieldsTwo,
getFieldByName: getFieldByNameFactory(fieldsTwo),
},
};
function testInitialState(): IndexPatternPrivateState {
return {
currentIndexPatternId: '1',
indexPatternRefs: [],
existingFields: {},
indexPatterns: expectedIndexPatterns,
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1'],
columns: {
col1: {
label: 'My Op',
dataType: 'string',
isBucketed: true,
// Private
operationType: 'terms',
sourceField: 'dest',
params: {
size: 5,
orderBy: { type: 'alphabetical' },
orderDirection: 'asc',
},
},
},
},
},
isFirstExistenceFetch: false,
};
}
describe('IndexPattern Data Source suggestions', () => {
beforeEach(async () => {
let count = 0;
jest.resetAllMocks();
(generateId as jest.Mock).mockImplementation(() => `id${++count}`);
});
describe('#getDatasourceSuggestionsForField', () => {
describe('with no layer', () => {
function stateWithoutLayer() {
return {
...testInitialState(),
layers: {},
};
}
it('should apply a bucketed aggregation for a string field, using metric for sorting', () => {
const suggestions = getDatasourceSuggestionsForField(stateWithoutLayer(), '1', {
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
id1: expect.objectContaining({
columnOrder: ['id3', 'id2'],
columns: {
id3: expect.objectContaining({
operationType: 'terms',
sourceField: 'source',
params: expect.objectContaining({
size: 5,
orderBy: { columnId: 'id2', type: 'column' },
}),
}),
id2: expect.objectContaining({
operationType: 'count',
}),
},
}),
},
}),
table: {
changeType: 'initial',
label: undefined,
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'id3',
}),
expect.objectContaining({
columnId: 'id2',
}),
],
layerId: 'id1',
},
})
);
});
it('should apply a bucketed aggregation for a date field', () => {
const suggestions = getDatasourceSuggestionsForField(stateWithoutLayer(), '1', {
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
id1: expect.objectContaining({
columnOrder: ['id3', 'id2'],
columns: {
id3: expect.objectContaining({
operationType: 'date_histogram',
sourceField: 'timestamp',
}),
id2: expect.objectContaining({
operationType: 'count',
}),
},
}),
},
}),
table: {
changeType: 'initial',
label: undefined,
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'id3',
}),
expect.objectContaining({
columnId: 'id2',
}),
],
layerId: 'id1',
},
})
);
});
it('should select a metric for a number field', () => {
const suggestions = getDatasourceSuggestionsForField(stateWithoutLayer(), '1', {
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
id1: expect.objectContaining({
columnOrder: ['id3', 'id2'],
columns: {
id3: expect.objectContaining({
operationType: 'date_histogram',
sourceField: 'timestamp',
}),
id2: expect.objectContaining({
operationType: 'avg',
sourceField: 'bytes',
}),
},
}),
},
}),
table: {
changeType: 'initial',
label: undefined,
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'id3',
}),
expect.objectContaining({
columnId: 'id2',
}),
],
layerId: 'id1',
},
})
);
});
it('should make a metric suggestion for a number field if there is no time field', async () => {
const state: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
currentIndexPatternId: '1',
isFirstExistenceFetch: false,
indexPatterns: {
1: {
id: '1',
title: 'no timefield',
hasRestrictions: false,
fields: [
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
],
getFieldByName: getFieldByNameFactory([
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
]),
},
},
layers: {
first: {
indexPatternId: '1',
columnOrder: [],
columns: {},
},
},
};
const suggestions = getDatasourceSuggestionsForField(state, '1', {
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
first: expect.objectContaining({
columnOrder: ['id1'],
columns: {
id1: expect.objectContaining({
operationType: 'avg',
sourceField: 'bytes',
}),
},
}),
},
}),
})
);
});
});
describe('with a previous empty layer', () => {
function stateWithEmptyLayer() {
const state = testInitialState();
return {
...state,
layers: {
previousLayer: {
indexPatternId: '1',
columns: {},
columnOrder: [],
},
},
};
}
it('should apply a bucketed aggregation for a string field, using metric for sorting', () => {
const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '1', {
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
previousLayer: expect.objectContaining({
columnOrder: ['id2', 'id1'],
columns: {
id2: expect.objectContaining({
operationType: 'terms',
sourceField: 'source',
params: expect.objectContaining({
size: 5,
orderBy: { columnId: 'id1', type: 'column' },
}),
}),
id1: expect.objectContaining({
operationType: 'count',
}),
},
}),
},
}),
table: {
changeType: 'initial',
label: undefined,
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'id2',
}),
expect.objectContaining({
columnId: 'id1',
}),
],
layerId: 'previousLayer',
},
})
);
});
it('should apply a bucketed aggregation for a date field', () => {
const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '1', {
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
previousLayer: expect.objectContaining({
columnOrder: ['id2', 'id1'],
columns: {
id2: expect.objectContaining({
operationType: 'date_histogram',
sourceField: 'timestamp',
}),
id1: expect.objectContaining({
operationType: 'count',
}),
},
}),
},
}),
table: {
changeType: 'initial',
label: undefined,
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'id2',
}),
expect.objectContaining({
columnId: 'id1',
}),
],
layerId: 'previousLayer',
},
})
);
});
it('should select a metric for a number field', () => {
const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '1', {
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
previousLayer: expect.objectContaining({
columnOrder: ['id2', 'id1'],
columns: {
id2: expect.objectContaining({
operationType: 'date_histogram',
sourceField: 'timestamp',
}),
id1: expect.objectContaining({
operationType: 'avg',
sourceField: 'bytes',
}),
},
}),
},
}),
table: {
changeType: 'initial',
label: undefined,
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'id2',
}),
expect.objectContaining({
columnId: 'id1',
}),
],
layerId: 'previousLayer',
},
})
);
});
it('should make a metric suggestion for a number field if there is no time field', async () => {
const state: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
currentIndexPatternId: '1',
isFirstExistenceFetch: false,
indexPatterns: {
1: {
id: '1',
title: 'no timefield',
hasRestrictions: false,
fields: [
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
],
getFieldByName: getFieldByNameFactory([
{
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
]),
},
},
layers: {
previousLayer: {
indexPatternId: '1',
columnOrder: [],
columns: {},
},
},
};
const suggestions = getDatasourceSuggestionsForField(state, '1', {
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
previousLayer: expect.objectContaining({
columnOrder: ['id1'],
columns: {
id1: expect.objectContaining({
operationType: 'avg',
sourceField: 'bytes',
}),
},
}),
},
}),
})
);
});
it('creates a new layer and replaces layer if no match is found', () => {
const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '2', {
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
previousLayer: expect.objectContaining({
indexPatternId: '1',
}),
id1: expect.objectContaining({
indexPatternId: '2',
}),
},
}),
table: {
changeType: 'initial',
label: undefined,
isMultiRow: true,
columns: expect.arrayContaining([]),
layerId: 'id1',
},
keptLayerIds: ['previousLayer'],
})
);
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
id1: expect.objectContaining({
indexPatternId: '2',
}),
},
}),
table: {
changeType: 'initial',
label: undefined,
isMultiRow: false,
columns: expect.arrayContaining([
expect.objectContaining({
columnId: expect.any(String),
}),
]),
layerId: 'id1',
},
keptLayerIds: [],
})
);
});
});
describe('suggesting extensions to non-empty tables', () => {
function stateWithNonEmptyTables(): IndexPatternPrivateState {
const state = testInitialState();
return {
...state,
currentIndexPatternId: '1',
layers: {
previousLayer: {
indexPatternId: '2',
columns: {},
columnOrder: [],
},
currentLayer: {
indexPatternId: '1',
columns: {
cola: {
dataType: 'string',
isBucketed: true,
sourceField: 'source',
label: 'values of source',
operationType: 'terms',
params: {
orderBy: { type: 'column', columnId: 'colb' },
orderDirection: 'asc',
size: 5,
},
},
colb: {
dataType: 'number',
isBucketed: false,
sourceField: 'bytes',
label: 'Avg of bytes',
operationType: 'avg',
},
},
columnOrder: ['cola', 'colb'],
},
},
};
}
it('replaces an existing date histogram column on date field', () => {
const initialState = stateWithNonEmptyTables();
const suggestions = getDatasourceSuggestionsForField(
{
...initialState,
layers: {
previousLayer: initialState.layers.previousLayer,
currentLayer: {
...initialState.layers.currentLayer,
columns: {
cola: {
dataType: 'date',
isBucketed: true,
sourceField: 'timestamp',
label: 'date histogram of timestamp',
operationType: 'date_histogram',
params: {
interval: 'w',
},
},
colb: {
dataType: 'number',
isBucketed: false,
sourceField: 'bytes',
label: 'Avg of bytes',
operationType: 'avg',
},
},
},
},
},
'1',
{
name: 'start_date',
displayName: 'start_date',
type: 'date',
aggregatable: true,
searchable: true,
}
);
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
previousLayer: initialState.layers.previousLayer,
currentLayer: expect.objectContaining({
columnOrder: ['cola', 'colb'],
columns: {
cola: expect.objectContaining({
operationType: 'date_histogram',
sourceField: 'start_date',
}),
colb: initialState.layers.currentLayer.columns.colb,
},
}),
},
}),
})
);
});
it('puts a date histogram column after the last bucket column on date field', () => {
const initialState = stateWithNonEmptyTables();
const suggestions = getDatasourceSuggestionsForField(initialState, '1', {
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
previousLayer: initialState.layers.previousLayer,
currentLayer: expect.objectContaining({
columnOrder: ['cola', 'id1', 'colb'],
columns: {
...initialState.layers.currentLayer.columns,
id1: expect.objectContaining({
operationType: 'date_histogram',
sourceField: 'timestamp',
}),
},
}),
},
}),
table: {
changeType: 'extended',
label: undefined,
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'cola',
}),
expect.objectContaining({
columnId: 'id1',
}),
expect.objectContaining({
columnId: 'colb',
}),
],
layerId: 'currentLayer',
},
})
);
});
it('does not use the same field for bucketing multiple times', () => {
const suggestions = getDatasourceSuggestionsForField(stateWithNonEmptyTables(), '1', {
name: 'source',
displayName: 'source',
type: 'string',
aggregatable: true,
searchable: true,
});
expect(suggestions).toHaveLength(1);
// Check that the suggestion is a single metric
expect(suggestions[0].table.columns).toHaveLength(1);
expect(suggestions[0].table.columns[0].operation.isBucketed).toBeFalsy();
});
it('appends a terms column with default size on string field', () => {
const initialState = stateWithNonEmptyTables();
const suggestions = getDatasourceSuggestionsForField(initialState, '1', {
name: 'dest',
displayName: 'dest',
type: 'string',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
previousLayer: initialState.layers.previousLayer,
currentLayer: expect.objectContaining({
columnOrder: ['cola', 'id1', 'colb'],
columns: {
...initialState.layers.currentLayer.columns,
id1: expect.objectContaining({
operationType: 'terms',
sourceField: 'dest',
params: expect.objectContaining({ size: 3 }),
}),
},
}),
},
}),
})
);
});
it('suggests both replacing and adding metric if only one other metric is set', () => {
const initialState = stateWithNonEmptyTables();
const suggestions = getDatasourceSuggestionsForField(initialState, '1', {
name: 'memory',
displayName: 'memory',
type: 'number',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: expect.objectContaining({
currentLayer: expect.objectContaining({
columnOrder: ['cola', 'colb'],
columns: {
cola: initialState.layers.currentLayer.columns.cola,
colb: expect.objectContaining({
operationType: 'avg',
sourceField: 'memory',
}),
},
}),
}),
}),
})
);
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: expect.objectContaining({
currentLayer: expect.objectContaining({
columnOrder: ['cola', 'colb', 'id1'],
columns: {
cola: initialState.layers.currentLayer.columns.cola,
colb: initialState.layers.currentLayer.columns.colb,
id1: expect.objectContaining({
operationType: 'avg',
sourceField: 'memory',
}),
},
}),
}),
}),
})
);
});
it('adds a metric column on a number field if no other metrics set', () => {
const initialState = stateWithNonEmptyTables();
const modifiedState: IndexPatternPrivateState = {
...initialState,
layers: {
...initialState.layers,
currentLayer: {
...initialState.layers.currentLayer,
columns: {
cola: initialState.layers.currentLayer.columns.cola,
},
columnOrder: ['cola'],
},
},
};
const suggestions = getDatasourceSuggestionsForField(modifiedState, '1', {
name: 'memory',
displayName: 'memory',
type: 'number',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
previousLayer: modifiedState.layers.previousLayer,
currentLayer: expect.objectContaining({
columnOrder: ['cola', 'id1'],
columns: {
...modifiedState.layers.currentLayer.columns,
id1: expect.objectContaining({
operationType: 'avg',
sourceField: 'memory',
}),
},
}),
},
}),
})
);
});
it('skips duplicates when the field is already in use', () => {
const initialState = stateWithNonEmptyTables();
const suggestions = getDatasourceSuggestionsForField(initialState, '1', {
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
});
expect(suggestions).not.toContain(expect.objectContaining({ changeType: 'extended' }));
});
it('skips duplicates when the document-specific field is already in use', () => {
const initialState = stateWithNonEmptyTables();
const modifiedState: IndexPatternPrivateState = {
...initialState,
layers: {
...initialState.layers,
currentLayer: {
...initialState.layers.currentLayer,
columns: {
...initialState.layers.currentLayer.columns,
colb: {
label: 'Count of records',
dataType: 'document',
isBucketed: false,
operationType: 'count',
sourceField: 'Records',
},
},
},
},
};
const suggestions = getDatasourceSuggestionsForField(modifiedState, '1', documentField);
expect(suggestions).not.toContain(expect.objectContaining({ changeType: 'extended' }));
});
});
describe('finding the layer that is using the current index pattern', () => {
function stateWithCurrentIndexPattern(): IndexPatternPrivateState {
const state = testInitialState();
return {
...state,
currentIndexPatternId: '1',
layers: {
previousLayer: {
indexPatternId: '1',
columns: {},
columnOrder: [],
},
currentLayer: {
indexPatternId: '2',
columns: {},
columnOrder: [],
},
},
};
}
it('suggests on the layer that matches by indexPatternId', () => {
const initialState = stateWithCurrentIndexPattern();
const suggestions = getDatasourceSuggestionsForField(initialState, '2', {
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
aggregationRestrictions: {
date_histogram: {
agg: 'date_histogram',
fixed_interval: '1d',
delay: '7d',
time_zone: 'UTC',
},
},
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
previousLayer: initialState.layers.previousLayer,
currentLayer: expect.objectContaining({
columnOrder: ['id2', 'id1'],
columns: {
id2: expect.objectContaining({
operationType: 'date_histogram',
sourceField: 'timestamp',
}),
id1: expect.objectContaining({
operationType: 'count',
}),
},
}),
},
}),
table: {
changeType: 'initial',
label: undefined,
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'id2',
}),
expect.objectContaining({
columnId: 'id1',
}),
],
layerId: 'currentLayer',
},
})
);
});
it('suggests on the layer with the fewest columns that matches by indexPatternId', () => {
const initialState = stateWithCurrentIndexPattern();
const suggestions = getDatasourceSuggestionsForField(initialState, '1', {
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
currentLayer: initialState.layers.currentLayer,
previousLayer: expect.objectContaining({
columnOrder: ['id2', 'id1'],
columns: {
id2: expect.objectContaining({
operationType: 'date_histogram',
sourceField: 'timestamp',
}),
id1: expect.objectContaining({
operationType: 'count',
}),
},
}),
},
}),
})
);
});
});
});
describe('#getDatasourceSuggestionsForVisualizeField', () => {
describe('with no layer', () => {
function stateWithoutLayer() {
return {
...testInitialState(),
layers: {},
};
}
it('should return an empty array if the field does not exist', () => {
const suggestions = getDatasourceSuggestionsForVisualizeField(
stateWithoutLayer(),
'1',
'field_not_exist'
);
expect(suggestions).toEqual([]);
});
it('should apply a bucketed aggregation for a string field', () => {
const suggestions = getDatasourceSuggestionsForVisualizeField(
stateWithoutLayer(),
'1',
'source'
);
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
id1: expect.objectContaining({
columnOrder: ['id3', 'id2'],
columns: {
id3: expect.objectContaining({
operationType: 'terms',
sourceField: 'source',
params: expect.objectContaining({ size: 5 }),
}),
id2: expect.objectContaining({
operationType: 'count',
}),
},
}),
},
}),
table: {
changeType: 'initial',
label: undefined,
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'id3',
}),
expect.objectContaining({
columnId: 'id2',
}),
],
layerId: 'id1',
},
})
);
});
});
});
describe('#getDatasourceSuggestionsFromCurrentState', () => {
it('returns no suggestions if there are no columns', () => {
expect(
getDatasourceSuggestionsFromCurrentState({
isFirstExistenceFetch: false,
indexPatternRefs: [],
existingFields: {},
indexPatterns: expectedIndexPatterns,
layers: {
first: {
indexPatternId: '1',
columnOrder: [],
columns: {},
},
},
currentIndexPatternId: '1',
})
).toEqual([]);
});
it('returns a single suggestion containing the current columns for each layer', async () => {
const initialState = testInitialState();
const state: IndexPatternPrivateState = {
...initialState,
layers: {
...initialState.layers,
second: {
...initialState.layers.first,
columnOrder: ['cola'],
columns: {
cola: {
label: 'My Op 2',
dataType: 'string',
isBucketed: true,
// Private
operationType: 'terms',
sourceField: 'dest',
params: {
size: 5,
orderBy: { type: 'alphabetical' },
orderDirection: 'asc',
},
},
},
},
},
};
const result = getDatasourceSuggestionsFromCurrentState(state);
expect(result).toContainEqual(
expect.objectContaining({
table: expect.objectContaining({
isMultiRow: true,
changeType: 'unchanged',
label: undefined,
layerId: 'first',
}),
keptLayerIds: ['first', 'second'],
})
);
expect(result).toContainEqual(
expect.objectContaining({
table: {
isMultiRow: true,
changeType: 'layers',
label: 'Show only layer 1',
columns: [
{
columnId: 'col1',
operation: {
label: 'My Op',
dataType: 'string',
isBucketed: true,
scale: undefined,
},
},
],
layerId: 'first',
},
})
);
expect(result).toContainEqual(
expect.objectContaining({
table: {
isMultiRow: true,
changeType: 'layers',
label: 'Show only layer 2',
columns: [
{
columnId: 'cola',
operation: {
label: 'My Op 2',
dataType: 'string',
isBucketed: true,
scale: undefined,
},
},
],
layerId: 'second',
},
})
);
});
it('returns a metric over time for single metric tables', async () => {
const initialState = testInitialState();
const state: IndexPatternPrivateState = {
...initialState,
layers: {
first: {
indexPatternId: '1',
columnOrder: ['cola'],
columns: {
cola: {
label: 'My Op',
dataType: 'number',
isBucketed: false,
operationType: 'avg',
sourceField: 'bytes',
scale: 'ratio',
},
},
},
},
};
expect(getDatasourceSuggestionsFromCurrentState(state)).toContainEqual(
expect.objectContaining({
table: {
isMultiRow: true,
changeType: 'extended',
label: 'Over time',
columns: [
{
columnId: 'id1',
operation: {
label: 'timestampLabel',
dataType: 'date',
isBucketed: true,
scale: 'interval',
},
},
{
columnId: 'cola',
operation: {
label: 'My Op',
dataType: 'number',
isBucketed: false,
scale: 'ratio',
},
},
],
layerId: 'first',
},
})
);
});
it('adds date histogram over default time field for tables without time dimension', async () => {
const initialState = testInitialState();
const state: IndexPatternPrivateState = {
...initialState,
layers: {
first: {
indexPatternId: '1',
columnOrder: ['cola', 'colb'],
columns: {
cola: {
label: 'My Terms',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
sourceField: 'source',
scale: 'ordinal',
params: {
orderBy: { type: 'alphabetical' },
orderDirection: 'asc',
size: 5,
},
},
colb: {
label: 'My Op',
dataType: 'number',
isBucketed: false,
operationType: 'avg',
sourceField: 'bytes',
scale: 'ratio',
},
},
},
},
};
expect(getDatasourceSuggestionsFromCurrentState(state)).toContainEqual(
expect.objectContaining({
table: {
isMultiRow: true,
changeType: 'extended',
label: 'Over time',
columns: [
{
columnId: 'cola',
operation: {
label: 'My Terms',
dataType: 'string',
isBucketed: true,
scale: 'ordinal',
},
},
{
columnId: 'id1',
operation: {
label: 'timestampLabel',
dataType: 'date',
isBucketed: true,
scale: 'interval',
},
},
{
columnId: 'colb',
operation: {
label: 'My Op',
dataType: 'number',
isBucketed: false,
scale: 'ratio',
},
},
],
layerId: 'first',
},
})
);
});
it('does not create an over time suggestion if tables with numeric buckets with time dimension', async () => {
const initialState = testInitialState();
const state: IndexPatternPrivateState = {
...initialState,
layers: {
first: {
indexPatternId: '1',
columnOrder: ['colb', 'cola'],
columns: {
cola: {
dataType: 'number',
isBucketed: false,
sourceField: 'dest',
label: 'Unique count of dest',
operationType: 'cardinality',
},
colb: {
label: 'My Op',
dataType: 'number',
isBucketed: true,
operationType: 'range',
sourceField: 'bytes',
scale: 'interval',
params: {
type: 'histogram',
maxBars: 100,
ranges: [],
},
},
},
},
},
};
expect(getDatasourceSuggestionsFromCurrentState(state)).not.toContainEqual(
expect.objectContaining({
table: {
isMultiRow: true,
label: 'Over time',
layerId: 'first',
},
})
);
});
it('adds date histogram over default time field for custom range intervals', async () => {
const initialState = testInitialState();
const state: IndexPatternPrivateState = {
...initialState,
layers: {
first: {
indexPatternId: '1',
columnOrder: ['colb', 'cola'],
columns: {
cola: {
dataType: 'number',
isBucketed: false,
sourceField: 'dest',
label: 'Unique count of dest',
operationType: 'cardinality',
},
colb: {
label: 'My Custom Range',
dataType: 'string',
isBucketed: true,
operationType: 'range',
sourceField: 'bytes',
scale: 'ordinal',
params: {
type: 'range',
maxBars: 100,
ranges: [{ from: 1, to: 2, label: '' }],
},
},
},
},
},
};
expect(getDatasourceSuggestionsFromCurrentState(state)).toContainEqual(
expect.objectContaining({
table: {
changeType: 'extended',
columns: [
{
columnId: 'colb',
operation: {
dataType: 'string',
isBucketed: true,
label: 'My Custom Range',
scale: 'ordinal',
},
},
{
columnId: 'id1',
operation: {
dataType: 'date',
isBucketed: true,
label: 'timestampLabel',
scale: 'interval',
},
},
{
columnId: 'cola',
operation: {
dataType: 'number',
isBucketed: false,
label: 'Unique count of dest',
scale: undefined,
},
},
],
isMultiRow: true,
label: 'Over time',
layerId: 'first',
},
})
);
});
it('does not create an over time suggestion if there is no default time field', async () => {
const initialState = testInitialState();
const state: IndexPatternPrivateState = {
...initialState,
layers: {
first: {
indexPatternId: '1',
columnOrder: ['id1'],
columns: {
id1: {
label: 'My Op',
dataType: 'number',
isBucketed: false,
operationType: 'avg',
sourceField: 'bytes',
scale: 'ratio',
},
},
},
},
};
const suggestions = getDatasourceSuggestionsFromCurrentState({
...state,
indexPatterns: { 1: { ...state.indexPatterns['1'], timeFieldName: undefined } },
});
suggestions.forEach((suggestion) => expect(suggestion.table.columns.length).toBe(1));
});
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: {},
currentIndexPatternId: '1',
indexPatterns: {
1: {
id: '1',
title: 'my-fake-index-pattern',
hasRestrictions: false,
fields,
getFieldByName: getFieldByNameFactory(fields),
},
},
isFirstExistenceFetch: false,
layers: {
first: {
...initialState.layers.first,
columns: {
col1: {
label: 'My Op',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
sourceField: 'field1',
params: {
size: 5,
orderBy: { type: 'alphabetical' },
orderDirection: 'asc',
},
},
col2: {
label: 'My Op',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
sourceField: 'field2',
params: {
size: 5,
orderBy: { type: 'alphabetical' },
orderDirection: 'asc',
},
},
col3: {
label: 'My Op',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
sourceField: 'field3',
params: {
size: 5,
orderBy: { type: 'alphabetical' },
orderDirection: 'asc',
},
},
col4: {
label: 'My Op',
dataType: 'number',
isBucketed: false,
operationType: 'avg',
sourceField: 'field4',
},
col5: {
label: 'My Op',
dataType: 'number',
isBucketed: false,
operationType: 'min',
sourceField: 'field5',
},
},
columnOrder: ['col1', 'col2', 'col3', 'col4', 'col5'],
},
},
};
const suggestions = getDatasourceSuggestionsFromCurrentState(state);
// 1 bucket col, 2 metric cols
isTableWithBucketColumns(suggestions[0], ['col1', 'col4', 'col5'], 1);
// 1 bucket col, 1 metric col
isTableWithBucketColumns(suggestions[1], ['col1', 'col4'], 1);
// 2 bucket cols, 2 metric cols
isTableWithBucketColumns(suggestions[2], ['col1', 'col2', 'col4', 'col5'], 2);
// 2 bucket cols, 1 metric col
isTableWithBucketColumns(suggestions[3], ['col1', 'col2', 'col4'], 2);
// 3 bucket cols, 2 metric cols
isTableWithBucketColumns(suggestions[4], ['col1', 'col2', 'col3', 'col4', 'col5'], 3);
// 3 bucket cols, 1 metric col
isTableWithBucketColumns(suggestions[5], ['col1', 'col2', 'col3', 'col4'], 3);
// first metric col
isTableWithMetricColumns(suggestions[6], ['col4']);
// second metric col
isTableWithMetricColumns(suggestions[7], ['col5']);
expect(suggestions.length).toBe(8);
});
it('returns an only metric version of a given table', () => {
const initialState = testInitialState();
const state: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
currentIndexPatternId: '1',
indexPatterns: {
1: {
id: '1',
title: 'my-fake-index-pattern',
hasRestrictions: false,
fields: [
{
name: 'field1',
displayName: 'field1',
type: 'number',
aggregatable: true,
searchable: true,
},
{
name: 'field2',
displayName: 'field2',
type: 'date',
aggregatable: true,
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,
layers: {
first: {
...initialState.layers.first,
columns: {
id1: {
label: 'Date histogram',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'field2',
params: {
interval: 'd',
},
},
id2: {
label: 'Average of field1',
dataType: 'number',
isBucketed: false,
operationType: 'avg',
sourceField: 'field1',
},
},
columnOrder: ['id1', 'id2'],
},
},
};
const suggestions = getDatasourceSuggestionsFromCurrentState(state);
expect(suggestions[1].table.columns[0].operation.label).toBe('Average of field1');
});
it('returns an alternative metric for an only-metric table', () => {
const initialState = testInitialState();
const state: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
currentIndexPatternId: '1',
indexPatterns: {
1: {
id: '1',
title: 'my-fake-index-pattern',
hasRestrictions: false,
fields: [
{
name: 'field1',
displayName: 'field1',
type: 'number',
aggregatable: true,
searchable: true,
},
],
getFieldByName: getFieldByNameFactory([
{
name: 'field1',
displayName: 'field1',
type: 'number',
aggregatable: true,
searchable: true,
},
]),
},
},
isFirstExistenceFetch: false,
layers: {
first: {
...initialState.layers.first,
columns: {
id1: {
label: 'Average of field1',
dataType: 'number',
isBucketed: false,
operationType: 'avg',
sourceField: 'field1',
},
},
columnOrder: ['id1'],
},
},
};
const suggestions = getDatasourceSuggestionsFromCurrentState(state);
expect(suggestions[0].table.columns.length).toBe(1);
expect(suggestions[0].table.columns[0].operation.label).toBe('Sum of field1');
});
it('contains a reordering suggestion when there are exactly 2 buckets', () => {
const initialState = testInitialState();
const state: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
currentIndexPatternId: '1',
indexPatterns: expectedIndexPatterns,
isFirstExistenceFetch: false,
layers: {
first: {
...initialState.layers.first,
columns: {
id1: {
label: 'Date histogram',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'timestamp',
params: {
interval: 'd',
},
},
id2: {
label: 'Top 5',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
sourceField: 'dest',
params: { size: 5, orderBy: { type: 'alphabetical' }, orderDirection: 'asc' },
},
id3: {
label: 'Average of field1',
dataType: 'number',
isBucketed: false,
operationType: 'avg',
sourceField: 'bytes',
},
},
columnOrder: ['id1', 'id2', 'id3'],
},
},
};
const suggestions = getDatasourceSuggestionsFromCurrentState(state);
expect(suggestions).toContainEqual(
expect.objectContaining({
table: expect.objectContaining({
changeType: 'reorder',
}),
})
);
});
it('does not generate suggestions if invalid fields are referenced', () => {
const initialState = testInitialState();
const state: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
currentIndexPatternId: '1',
indexPatterns: expectedIndexPatterns,
isFirstExistenceFetch: false,
layers: {
first: {
...initialState.layers.first,
columns: {
...initialState.layers.first.columns,
col2: {
label: 'Top 5',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
sourceField: 'nonExistingField',
params: { size: 5, orderBy: { type: 'alphabetical' }, orderDirection: 'asc' },
},
},
columnOrder: ['col1', 'col2'],
},
},
};
const suggestions = getDatasourceSuggestionsFromCurrentState(state);
expect(suggestions).toEqual([]);
});
});
});
function isTableWithBucketColumns(
suggestion: DatasourceSuggestion<IndexPatternPrivateState>,
columnIds: string[],
numBuckets: number
) {
expect(suggestion.table.columns.map((column) => column.columnId)).toEqual(columnIds);
expect(
suggestion.table.columns.slice(0, numBuckets).every((column) => column.operation.isBucketed)
).toBeTruthy();
}
function isTableWithMetricColumns(
suggestion: DatasourceSuggestion<IndexPatternPrivateState>,
columnIds: string[]
) {
expect(suggestion.table.isMultiRow).toEqual(false);
expect(suggestion.table.columns.map((column) => column.columnId)).toEqual(columnIds);
expect(suggestion.table.columns.every((column) => !column.operation.isBucketed)).toBeTruthy();
}