kibana/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
Marta Bondyra b87852071b
[Lens] fix passing 0 as static value (#118032)
* [Lens] fix passing 0 as static value

* allow computed static_value to be passed

* Update x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx

Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>

* ci fix

Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
2021-11-11 08:26:48 +01:00

1710 lines
52 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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import 'jest-canvas-mock';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { getIndexPatternDatasource, IndexPatternColumn } from './indexpattern';
import { DatasourcePublicAPI, Operation, Datasource, FramePublicAPI } from '../types';
import { coreMock } from 'src/core/public/mocks';
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';
import { operationDefinitionMap, getErrorMessages } from './operations';
import { createMockedFullReference } from './operations/mocks';
import { indexPatternFieldEditorPluginMock } from 'src/plugins/index_pattern_field_editor/public/mocks';
import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks';
import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks';
jest.mock('./loader');
jest.mock('../id_generator');
jest.mock('./operations');
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: fieldsOne,
getFieldByName: getFieldByNameFactory(fieldsOne),
},
2: {
id: '2',
title: 'my-fake-restricted-pattern',
timeFieldName: 'timestamp',
hasRestrictions: true,
fields: fieldsTwo,
getFieldByName: getFieldByNameFactory(fieldsTwo),
},
};
type IndexPatternBaseState = Omit<
IndexPatternPrivateState,
'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch'
>;
function enrichBaseState(baseState: IndexPatternBaseState): IndexPatternPrivateState {
return {
currentIndexPatternId: baseState.currentIndexPatternId,
layers: baseState.layers,
indexPatterns: expectedIndexPatterns,
indexPatternRefs: [],
existingFields: {},
isFirstExistenceFetch: false,
};
}
describe('IndexPattern Data Source', () => {
let baseState: Omit<
IndexPatternPrivateState,
'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch'
>;
let indexPatternDatasource: Datasource<IndexPatternPrivateState, IndexPatternPersistedState>;
beforeEach(() => {
indexPatternDatasource = getIndexPatternDatasource({
storage: {} as IStorageWrapper,
core: coreMock.createStart(),
data: dataPluginMock.createStartContract(),
fieldFormats: fieldFormatsServiceMock.createStartContract(),
charts: chartPluginMock.createSetupContract(),
indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(),
uiActions: uiActionsPluginMock.createStartContract(),
});
baseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1'],
columns: {
col1: {
label: 'My Op',
dataType: 'string',
isBucketed: true,
// Private
operationType: 'terms',
sourceField: 'op',
params: {
size: 5,
orderBy: { type: 'alphabetical' },
orderDirection: 'asc',
},
},
},
},
},
};
});
describe('uniqueLabels', () => {
it('appends a suffix to duplicates', () => {
const col: IndexPatternColumn = {
dataType: 'number',
isBucketed: false,
label: 'Foo',
operationType: 'count',
sourceField: 'Records',
};
const map = indexPatternDatasource.uniqueLabels({
layers: {
a: {
columnOrder: ['a', 'b'],
columns: {
a: col,
b: col,
},
indexPatternId: 'foo',
},
b: {
columnOrder: ['c', 'd'],
columns: {
c: col,
d: {
...col,
label: 'Foo [1]',
},
},
indexPatternId: 'foo',
},
},
} as unknown as IndexPatternPrivateState);
expect(map).toMatchInlineSnapshot(`
Object {
"a": "Foo",
"b": "Foo [1]",
"c": "Foo [2]",
"d": "Foo [1] [1]",
}
`);
});
});
describe('#getPersistedState', () => {
it('should persist from saved state', async () => {
const state = enrichBaseState(baseState);
expect(indexPatternDatasource.getPersistableState(state)).toEqual({
state: {
layers: {
first: {
columnOrder: ['col1'],
columns: {
col1: {
label: 'My Op',
dataType: 'string',
isBucketed: true,
// Private
operationType: 'terms',
sourceField: 'op',
params: {
size: 5,
orderBy: { type: 'alphabetical' },
orderDirection: 'asc',
},
},
},
},
},
},
savedObjectReferences: [
{ name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', id: '1' },
{ name: 'indexpattern-datasource-layer-first', type: 'index-pattern', id: '1' },
],
});
});
});
describe('#toExpression', () => {
it('should generate an empty expression when no columns are selected', async () => {
const state = await indexPatternDatasource.initialize();
expect(indexPatternDatasource.toExpression(state, 'first')).toEqual(null);
});
it('should create a table when there is a formula without aggs', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1'],
columns: {
col1: {
label: 'Formula',
dataType: 'number',
isBucketed: false,
operationType: 'formula',
references: [],
params: {},
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
expect(indexPatternDatasource.toExpression(state, 'first')).toEqual({
chain: [
{
function: 'createTable',
type: 'function',
arguments: { ids: [], names: [], rowCount: [1] },
},
{
arguments: { expression: [''], id: ['col1'], name: ['Formula'] },
function: 'mapColumn',
type: 'function',
},
],
type: 'expression',
});
});
it('should generate an expression for an aggregated query', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1', 'col2'],
columns: {
col1: {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
},
col2: {
label: 'Date',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'timestamp',
params: {
interval: '1d',
},
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
expect(indexPatternDatasource.toExpression(state, 'first')).toMatchInlineSnapshot(`
Object {
"chain": Array [
Object {
"arguments": Object {
"aggs": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"enabled": Array [
true,
],
"id": Array [
"0",
],
"schema": Array [
"metric",
],
},
"function": "aggCount",
"type": "function",
},
],
"type": "expression",
},
Object {
"chain": Array [
Object {
"arguments": Object {
"drop_partials": Array [
false,
],
"enabled": Array [
true,
],
"extended_bounds": Array [
Object {
"chain": Array [
Object {
"arguments": Object {},
"function": "extendedBounds",
"type": "function",
},
],
"type": "expression",
},
],
"field": Array [
"timestamp",
],
"id": Array [
"1",
],
"interval": Array [
"1d",
],
"min_doc_count": Array [
0,
],
"schema": Array [
"segment",
],
"useNormalizedEsInterval": Array [
true,
],
},
"function": "aggDateHistogram",
"type": "function",
},
],
"type": "expression",
},
],
"index": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"id": Array [
"1",
],
},
"function": "indexPatternLoad",
"type": "function",
},
],
"type": "expression",
},
],
"metricsAtAllLevels": Array [
false,
],
"partialRows": Array [
false,
],
"timeFields": Array [
"timestamp",
],
},
"function": "esaggs",
"type": "function",
},
Object {
"arguments": Object {
"idMap": Array [
"{\\"col-0-0\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-1\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}",
],
},
"function": "lens_rename_columns",
"type": "function",
},
],
"type": "expression",
}
`);
});
it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1', 'col2', 'col3'],
columns: {
col1: {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
},
col2: {
label: 'Date',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'timestamp',
params: {
interval: 'auto',
},
},
col3: {
label: 'Date 2',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'another_datefield',
params: {
interval: 'auto',
},
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']);
});
it('should pass time shift parameter to metric agg functions', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col2', 'col1'],
columns: {
col1: {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
timeShift: '1d',
},
col2: {
label: 'Date',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'timestamp',
params: {
interval: 'auto',
},
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
expect((ast.chain[0].arguments.aggs[1] as Ast).chain[0].arguments.timeShift).toEqual(['1d']);
});
it('should wrap filtered metrics in filtered metric aggregation', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1', 'col2', 'col3'],
columns: {
col1: {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
timeScale: 'h',
filter: {
language: 'kuery',
query: 'bytes > 5',
},
},
col2: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
sourceField: 'bytes',
operationType: 'average',
timeScale: 'h',
},
col3: {
label: 'Date',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'timestamp',
params: {
interval: 'auto',
},
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
expect(ast.chain[0].arguments.aggs[0]).toMatchInlineSnapshot(`
Object {
"chain": Array [
Object {
"arguments": Object {
"customBucket": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"enabled": Array [
true,
],
"filter": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"q": Array [
"bytes > 5",
],
},
"function": "kql",
"type": "function",
},
],
"type": "expression",
},
],
"id": Array [
"0-filter",
],
"schema": Array [
"bucket",
],
},
"function": "aggFilter",
"type": "function",
},
],
"type": "expression",
},
],
"customMetric": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"enabled": Array [
true,
],
"id": Array [
"0-metric",
],
"schema": Array [
"metric",
],
},
"function": "aggCount",
"type": "function",
},
],
"type": "expression",
},
],
"enabled": Array [
true,
],
"id": Array [
"0",
],
"schema": Array [
"metric",
],
},
"function": "aggFilteredMetric",
"type": "function",
},
],
"type": "expression",
}
`);
});
it('should add time_scale and format function if time scale is set and supported', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1', 'col2', 'col3'],
columns: {
col1: {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
timeScale: 'h',
},
col2: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
sourceField: 'bytes',
operationType: 'average',
timeScale: 'h',
},
col3: {
label: 'Date',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'timestamp',
params: {
interval: 'auto',
},
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
const timeScaleCalls = ast.chain.filter((fn) => fn.function === 'lens_time_scale');
const formatCalls = ast.chain.filter((fn) => fn.function === 'lens_format_column');
expect(timeScaleCalls).toHaveLength(1);
expect(timeScaleCalls[0].arguments).toMatchInlineSnapshot(`
Object {
"dateColumnId": Array [
"col3",
],
"inputColumnId": Array [
"col1",
],
"outputColumnId": Array [
"col1",
],
"outputColumnName": Array [
"Count of records",
],
"targetUnit": Array [
"h",
],
}
`);
expect(formatCalls[0]).toMatchInlineSnapshot(`
Object {
"arguments": Object {
"columnId": Array [
"col1",
],
"format": Array [
"",
],
"parentFormat": Array [
"{\\"id\\":\\"suffix\\",\\"params\\":{\\"unit\\":\\"h\\"}}",
],
},
"function": "lens_format_column",
"type": "function",
}
`);
});
it('should put column formatters after calculated columns', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['bucket', 'metric', 'calculated'],
columns: {
bucket: {
label: 'Date',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'timestamp',
params: {
interval: 'auto',
},
},
metric: {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
timeScale: 'h',
},
calculated: {
label: 'Moving average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'moving_average',
references: ['metric'],
params: {
window: 5,
},
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
const formatIndex = ast.chain.findIndex((fn) => fn.function === 'lens_format_column');
const calculationIndex = ast.chain.findIndex((fn) => fn.function === 'moving_average');
expect(calculationIndex).toBeLessThan(formatIndex);
});
it('should rename the output from esaggs when using flat query', () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['bucket1', 'bucket2', 'metric'],
columns: {
metric: {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
},
bucket1: {
label: 'Date',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'timestamp',
params: {
interval: '1d',
},
},
bucket2: {
label: 'Terms',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
sourceField: 'geo.src',
params: {
orderBy: { type: 'alphabetical' },
orderDirection: 'asc',
size: 10,
},
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
expect(ast.chain[0].arguments.metricsAtAllLevels).toEqual([false]);
expect(JSON.parse(ast.chain[1].arguments.idMap[0] as string)).toEqual({
'col-0-0': expect.objectContaining({ id: 'bucket1' }),
'col-1-1': expect.objectContaining({ id: 'bucket2' }),
'col-2-2': expect.objectContaining({ id: 'metric' }),
});
});
it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1', 'col2'],
columns: {
col1: {
label: 'Count of records',
dataType: 'date',
isBucketed: false,
sourceField: 'timefield',
operationType: 'unique_count',
},
col2: {
label: 'Date',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'timestamp',
params: {
interval: 'auto',
},
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp']);
expect(ast.chain[0].arguments.timeFields).not.toContain('timefield');
});
describe('references', () => {
beforeEach(() => {
// @ts-expect-error we are inserting an invalid type
operationDefinitionMap.testReference = createMockedFullReference();
// @ts-expect-error we are inserting an invalid type
operationDefinitionMap.testReference.toExpression.mockReturnValue(['mock']);
});
afterEach(() => {
delete operationDefinitionMap.testReference;
});
it('should collect expression references and append them', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1', 'col2'],
columns: {
col1: {
label: 'Count of records',
dataType: 'date',
isBucketed: false,
sourceField: 'timefield',
operationType: 'unique_count',
},
col2: {
label: 'Reference',
dataType: 'number',
isBucketed: false,
// @ts-expect-error not a valid type
operationType: 'testReference',
references: ['col1'],
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
// @ts-expect-error we can't isolate just the reference type
expect(operationDefinitionMap.testReference.toExpression).toHaveBeenCalled();
expect(ast.chain[2]).toEqual('mock');
});
it('should keep correct column mapping keys with reference columns present', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col2', 'col1'],
columns: {
col1: {
label: 'Count of records',
dataType: 'date',
isBucketed: false,
sourceField: 'timefield',
operationType: 'unique_count',
},
col2: {
label: 'Reference',
dataType: 'number',
isBucketed: false,
// @ts-expect-error not a valid type
operationType: 'testReference',
references: ['col1'],
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
expect(JSON.parse(ast.chain[1].arguments.idMap[0] as string)).toEqual({
'col-0-0': expect.objectContaining({
id: 'col1',
}),
});
});
it('should topologically sort references', () => {
// This is a real example of count() + count()
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['date', 'count', 'formula', 'countX0', 'math'],
columns: {
count: {
label: 'count',
dataType: 'number',
operationType: 'count',
isBucketed: false,
scale: 'ratio',
sourceField: 'Records',
customLabel: true,
},
date: {
label: 'timestamp',
dataType: 'date',
operationType: 'date_histogram',
sourceField: 'timestamp',
isBucketed: true,
scale: 'interval',
params: {
interval: 'auto',
},
},
formula: {
label: 'Formula',
dataType: 'number',
operationType: 'formula',
isBucketed: false,
scale: 'ratio',
params: {
formula: 'count() + count()',
isFormulaBroken: false,
},
references: ['math'],
},
countX0: {
label: 'countX0',
dataType: 'number',
operationType: 'count',
isBucketed: false,
scale: 'ratio',
sourceField: 'Records',
customLabel: true,
},
math: {
label: 'math',
dataType: 'number',
operationType: 'math',
isBucketed: false,
scale: 'ratio',
params: {
tinymathAst: {
type: 'function',
name: 'add',
// @ts-expect-error String args are not valid tinymath, but signals something unique to Lens
args: ['countX0', 'count'],
location: {
min: 0,
max: 17,
},
text: 'count() + count()',
},
},
references: ['countX0', 'count'],
customLabel: true,
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
const chainLength = ast.chain.length;
expect(ast.chain[chainLength - 2].arguments.name).toEqual(['math']);
expect(ast.chain[chainLength - 1].arguments.id).toEqual(['formula']);
});
});
});
describe('#insertLayer', () => {
it('should insert an empty layer into the previous state', () => {
const state = {
indexPatternRefs: [],
existingFields: {},
indexPatterns: expectedIndexPatterns,
layers: {
first: {
indexPatternId: '1',
columnOrder: [],
columns: {},
},
second: {
indexPatternId: '2',
columnOrder: [],
columns: {},
},
},
currentIndexPatternId: '1',
isFirstExistenceFetch: false,
};
expect(indexPatternDatasource.insertLayer(state, 'newLayer')).toEqual({
...state,
layers: {
...state.layers,
newLayer: {
indexPatternId: '1',
columnOrder: [],
columns: {},
},
},
});
});
});
describe('#removeLayer', () => {
it('should remove a layer', () => {
const state = {
indexPatternRefs: [],
existingFields: {},
isFirstExistenceFetch: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {
indexPatternId: '1',
columnOrder: [],
columns: {},
},
second: {
indexPatternId: '2',
columnOrder: [],
columns: {},
},
},
currentIndexPatternId: '1',
};
expect(indexPatternDatasource.removeLayer(state, 'first')).toEqual({
...state,
layers: {
second: {
indexPatternId: '2',
columnOrder: [],
columns: {},
},
},
});
});
});
describe('#getLayers', () => {
it('should list the current layers', () => {
expect(
indexPatternDatasource.getLayers({
indexPatternRefs: [],
existingFields: {},
isFirstExistenceFetch: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {
indexPatternId: '1',
columnOrder: [],
columns: {},
},
second: {
indexPatternId: '2',
columnOrder: [],
columns: {},
},
},
currentIndexPatternId: '1',
})
).toEqual(['first', 'second']);
});
});
describe('#getPublicAPI', () => {
let publicAPI: DatasourcePublicAPI;
beforeEach(async () => {
const initialState = enrichBaseState(baseState);
publicAPI = indexPatternDatasource.getPublicAPI({
state: initialState,
layerId: 'first',
});
});
describe('getTableSpec', () => {
it('should include col1', () => {
expect(publicAPI.getTableSpec()).toEqual([{ columnId: 'col1' }]);
});
it('should skip columns that are being referenced', () => {
publicAPI = indexPatternDatasource.getPublicAPI({
state: {
...enrichBaseState(baseState),
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1', 'col2'],
columns: {
col1: {
label: 'Sum',
dataType: 'number',
isBucketed: false,
operationType: 'sum',
sourceField: 'test',
params: {},
} as IndexPatternColumn,
col2: {
label: 'Cumulative sum',
dataType: 'number',
isBucketed: false,
operationType: 'cumulative_sum',
references: ['col1'],
params: {},
} as IndexPatternColumn,
},
},
},
},
layerId: 'first',
});
expect(publicAPI.getTableSpec()).toEqual([{ columnId: 'col2' }]);
});
});
describe('getOperationForColumnId', () => {
it('should get an operation for col1', () => {
expect(publicAPI.getOperationForColumnId('col1')).toEqual({
label: 'My Op',
dataType: 'string',
isBucketed: true,
} as Operation);
});
it('should return null for non-existant columns', () => {
expect(publicAPI.getOperationForColumnId('col2')).toBe(null);
});
it('should return null for referenced columns', () => {
publicAPI = indexPatternDatasource.getPublicAPI({
state: {
...enrichBaseState(baseState),
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1', 'col2'],
columns: {
col1: {
label: 'Sum',
dataType: 'number',
isBucketed: false,
operationType: 'sum',
sourceField: 'test',
params: {},
} as IndexPatternColumn,
col2: {
label: 'Cumulative sum',
dataType: 'number',
isBucketed: false,
operationType: 'cumulative_sum',
references: ['col1'],
params: {},
} as IndexPatternColumn,
},
},
},
},
layerId: 'first',
});
expect(publicAPI.getOperationForColumnId('col1')).toEqual(null);
});
});
});
describe('#getErrorMessages', () => {
it('should use the results of getErrorMessages directly when single layer', () => {
(getErrorMessages as jest.Mock).mockClear();
(getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']);
const state: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
isFirstExistenceFetch: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {
indexPatternId: '1',
columnOrder: [],
columns: {},
},
},
currentIndexPatternId: '1',
};
expect(indexPatternDatasource.getErrorMessages(state)).toEqual([
{ longMessage: 'error 1', shortMessage: '' },
{ longMessage: 'error 2', shortMessage: '' },
]);
expect(getErrorMessages).toHaveBeenCalledTimes(1);
});
it('should prepend each error with its layer number on multi-layer chart', () => {
(getErrorMessages as jest.Mock).mockClear();
(getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']);
const state: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
isFirstExistenceFetch: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {
indexPatternId: '1',
columnOrder: [],
columns: {},
},
second: {
indexPatternId: '1',
columnOrder: [],
columns: {},
},
},
currentIndexPatternId: '1',
};
expect(indexPatternDatasource.getErrorMessages(state)).toEqual([
{ longMessage: 'Layer 1 error: error 1', shortMessage: '' },
{ longMessage: 'Layer 1 error: error 2', shortMessage: '' },
]);
expect(getErrorMessages).toHaveBeenCalledTimes(2);
});
});
describe('#getWarningMessages', () => {
it('should return mismatched time shifts', () => {
const state: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
isFirstExistenceFetch: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1', 'col2', 'col3', 'col4', 'col5', 'col6'],
columns: {
col1: {
operationType: 'date_histogram',
params: {
interval: '12h',
},
label: '',
dataType: 'date',
isBucketed: true,
sourceField: 'timestamp',
},
col2: {
operationType: 'count',
label: '',
dataType: 'number',
isBucketed: false,
sourceField: 'records',
},
col3: {
operationType: 'count',
timeShift: '1h',
label: '',
dataType: 'number',
isBucketed: false,
sourceField: 'records',
},
col4: {
operationType: 'count',
timeShift: '13h',
label: '',
dataType: 'number',
isBucketed: false,
sourceField: 'records',
},
col5: {
operationType: 'count',
timeShift: '1w',
label: '',
dataType: 'number',
isBucketed: false,
sourceField: 'records',
},
col6: {
operationType: 'count',
timeShift: 'previous',
label: '',
dataType: 'number',
isBucketed: false,
sourceField: 'records',
},
},
},
},
currentIndexPatternId: '1',
};
const warnings = indexPatternDatasource.getWarningMessages!(state, {
activeData: {
first: {
type: 'datatable',
rows: [],
columns: [
{
id: 'col1',
name: 'col1',
meta: {
type: 'date',
source: 'esaggs',
sourceParams: {
type: 'date_histogram',
params: {
used_interval: '12h',
},
},
},
},
],
},
},
} as unknown as FramePublicAPI);
expect(warnings!.length).toBe(2);
expect((warnings![0] as React.ReactElement).props.id).toEqual(
'xpack.lens.indexPattern.timeShiftSmallWarning'
);
expect((warnings![1] as React.ReactElement).props.id).toEqual(
'xpack.lens.indexPattern.timeShiftMultipleWarning'
);
});
it('should prepend each error with its layer number on multi-layer chart', () => {
(getErrorMessages as jest.Mock).mockClear();
(getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']);
const state: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
isFirstExistenceFetch: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {
indexPatternId: '1',
columnOrder: [],
columns: {},
},
second: {
indexPatternId: '1',
columnOrder: [],
columns: {},
},
},
currentIndexPatternId: '1',
};
expect(indexPatternDatasource.getErrorMessages(state)).toEqual([
{ longMessage: 'Layer 1 error: error 1', shortMessage: '' },
{ longMessage: 'Layer 1 error: error 2', shortMessage: '' },
]);
expect(getErrorMessages).toHaveBeenCalledTimes(2);
});
});
describe('#updateStateOnCloseDimension', () => {
it('should not update when there are no incomplete columns', () => {
expect(
indexPatternDatasource.updateStateOnCloseDimension!({
state: {
indexPatternRefs: [],
existingFields: {},
isFirstExistenceFetch: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1'],
columns: {
col1: {
dataType: 'number',
isBucketed: false,
label: 'Foo',
operationType: 'average',
sourceField: 'bytes',
},
},
incompleteColumns: {},
},
},
currentIndexPatternId: '1',
},
layerId: 'first',
columnId: 'col1',
})
).toBeUndefined();
});
it('should clear all incomplete columns', () => {
const state = {
indexPatternRefs: [],
existingFields: {},
isFirstExistenceFetch: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {
indexPatternId: '1',
columnOrder: [],
columns: {},
incompleteColumns: {
col1: { operationType: 'average' as const },
col2: { operationType: 'sum' as const },
},
},
},
currentIndexPatternId: '1',
};
expect(
indexPatternDatasource.updateStateOnCloseDimension!({
state,
layerId: 'first',
columnId: 'col1',
})
).toEqual({
...state,
layers: {
first: {
indexPatternId: '1',
columnOrder: [],
columns: {},
incompleteColumns: undefined,
},
},
});
});
});
describe('#isTimeBased', () => {
it('should return true if date histogram exists in any layer', () => {
const state = enrichBaseState({
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['metric'],
columns: {
metric: {
label: 'Count of records2',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
},
},
},
second: {
indexPatternId: '1',
columnOrder: ['bucket1', 'bucket2', 'metric2'],
columns: {
metric2: {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
},
bucket1: {
label: 'Date',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'timestamp',
params: {
interval: '1d',
},
},
bucket2: {
label: 'Terms',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
sourceField: 'geo.src',
params: {
orderBy: { type: 'alphabetical' },
orderDirection: 'asc',
size: 10,
},
},
},
},
},
});
expect(indexPatternDatasource.isTimeBased(state)).toEqual(true);
});
it('should return false if date histogram does not exist in any layer', () => {
const state = enrichBaseState({
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['metric'],
columns: {
metric: {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
},
},
},
},
});
expect(indexPatternDatasource.isTimeBased(state)).toEqual(false);
});
});
describe('#initializeDimension', () => {
it('should return the same state if no static value is passed', () => {
const state = enrichBaseState({
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['metric'],
columns: {
metric: {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
},
},
},
},
});
expect(
indexPatternDatasource.initializeDimension!(state, 'first', {
columnId: 'newStatic',
label: 'MyNewColumn',
groupId: 'a',
dataType: 'number',
})
).toBe(state);
});
it('should add a new static value column if a static value is passed', () => {
const state = enrichBaseState({
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['metric'],
columns: {
metric: {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
},
},
},
},
});
expect(
indexPatternDatasource.initializeDimension!(state, 'first', {
columnId: 'newStatic',
label: 'MyNewColumn',
groupId: 'a',
dataType: 'number',
staticValue: 0, // use a falsy value to check also this corner case
})
).toEqual({
...state,
layers: {
...state.layers,
first: {
...state.layers.first,
incompleteColumns: {},
columnOrder: ['metric', 'newStatic'],
columns: {
...state.layers.first.columns,
newStatic: {
dataType: 'number',
isBucketed: false,
label: 'Static value: 0',
operationType: 'static_value',
params: { value: '0' },
references: [],
scale: 'ratio',
},
},
},
},
});
});
});
});