1965 lines
57 KiB
TypeScript
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();
|
|
}
|