2019-09-17 20:57:53 +02:00
|
|
|
/*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2020-08-14 16:54:43 +02:00
|
|
|
import { Ast } from '@kbn/interpreter/common';
|
|
|
|
import { buildExpression } from '../../../../../src/plugins/expressions/public';
|
2020-11-04 11:27:52 +01:00
|
|
|
import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
|
2020-03-17 14:57:52 +01:00
|
|
|
import { DatatableVisualizationState, datatableVisualization } from './visualization';
|
2019-10-30 00:05:03 +01:00
|
|
|
import { Operation, DataType, FramePublicAPI, TableSuggestionColumn } from '../types';
|
2019-09-17 20:57:53 +02:00
|
|
|
|
|
|
|
function mockFrame(): FramePublicAPI {
|
|
|
|
return {
|
2020-11-04 11:27:52 +01:00
|
|
|
...createMockFramePublicAPI(),
|
2019-09-17 20:57:53 +02:00
|
|
|
addNewLayer: () => 'aaa',
|
|
|
|
removeLayers: () => {},
|
|
|
|
datasourceLayers: {},
|
|
|
|
query: { query: '', language: 'lucene' },
|
|
|
|
dateRange: {
|
|
|
|
fromDate: 'now-7d',
|
|
|
|
toDate: 'now',
|
|
|
|
},
|
2019-10-03 17:24:45 +02:00
|
|
|
filters: [],
|
2019-09-17 20:57:53 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
describe('Datatable Visualization', () => {
|
|
|
|
describe('#initialize', () => {
|
|
|
|
it('should initialize from the empty state', () => {
|
|
|
|
expect(datatableVisualization.initialize(mockFrame(), undefined)).toEqual({
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'aaa',
|
2020-03-17 14:57:52 +01:00
|
|
|
columns: [],
|
2019-09-17 20:57:53 +02:00
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should initialize from a persisted state', () => {
|
|
|
|
const expectedState: DatatableVisualizationState = {
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'foo',
|
|
|
|
columns: ['saved'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
expect(datatableVisualization.initialize(mockFrame(), expectedState)).toEqual(expectedState);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-01-14 18:06:51 +01:00
|
|
|
describe('#getLayerIds', () => {
|
|
|
|
it('return the layer ids', () => {
|
|
|
|
const state: DatatableVisualizationState = {
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'baz',
|
|
|
|
columns: ['a', 'b', 'c'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
expect(datatableVisualization.getLayerIds(state)).toEqual(['baz']);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('#clearLayer', () => {
|
|
|
|
it('should reset the layer', () => {
|
|
|
|
const state: DatatableVisualizationState = {
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'baz',
|
|
|
|
columns: ['a', 'b', 'c'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
expect(datatableVisualization.clearLayer(state, 'baz')).toMatchObject({
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'baz',
|
2020-03-17 14:57:52 +01:00
|
|
|
columns: [],
|
2020-01-14 18:06:51 +01:00
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-10-30 00:05:03 +01:00
|
|
|
describe('#getSuggestions', () => {
|
|
|
|
function numCol(columnId: string): TableSuggestionColumn {
|
|
|
|
return {
|
|
|
|
columnId,
|
|
|
|
operation: {
|
|
|
|
dataType: 'number',
|
|
|
|
label: `Avg ${columnId}`,
|
|
|
|
isBucketed: false,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function strCol(columnId: string): TableSuggestionColumn {
|
|
|
|
return {
|
|
|
|
columnId,
|
|
|
|
operation: {
|
|
|
|
dataType: 'string',
|
|
|
|
label: `Top 5 ${columnId}`,
|
|
|
|
isBucketed: true,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
it('should accept a single-layer suggestion', () => {
|
|
|
|
const suggestions = datatableVisualization.getSuggestions({
|
|
|
|
state: {
|
|
|
|
layers: [{ layerId: 'first', columns: ['col1'] }],
|
|
|
|
},
|
|
|
|
table: {
|
|
|
|
isMultiRow: true,
|
|
|
|
layerId: 'first',
|
|
|
|
changeType: 'initial',
|
|
|
|
columns: [numCol('col1'), strCol('col2')],
|
|
|
|
},
|
|
|
|
keptLayerIds: [],
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(suggestions.length).toBeGreaterThan(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not make suggestions when the table is unchanged', () => {
|
|
|
|
const suggestions = datatableVisualization.getSuggestions({
|
|
|
|
state: {
|
|
|
|
layers: [{ layerId: 'first', columns: ['col1'] }],
|
|
|
|
},
|
|
|
|
table: {
|
|
|
|
isMultiRow: true,
|
|
|
|
layerId: 'first',
|
|
|
|
changeType: 'unchanged',
|
|
|
|
columns: [numCol('col1')],
|
|
|
|
},
|
|
|
|
keptLayerIds: ['first'],
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(suggestions).toEqual([]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not make suggestions when multiple layers are involved', () => {
|
|
|
|
const suggestions = datatableVisualization.getSuggestions({
|
|
|
|
state: {
|
|
|
|
layers: [{ layerId: 'first', columns: ['col1'] }],
|
|
|
|
},
|
|
|
|
table: {
|
|
|
|
isMultiRow: true,
|
|
|
|
layerId: 'first',
|
|
|
|
changeType: 'unchanged',
|
|
|
|
columns: [numCol('col1')],
|
|
|
|
},
|
|
|
|
keptLayerIds: ['first', 'second'],
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(suggestions).toEqual([]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not make suggestions when the suggestion keeps a different layer', () => {
|
|
|
|
const suggestions = datatableVisualization.getSuggestions({
|
|
|
|
state: {
|
|
|
|
layers: [{ layerId: 'older', columns: ['col1'] }],
|
|
|
|
},
|
|
|
|
table: {
|
|
|
|
isMultiRow: true,
|
|
|
|
layerId: 'newer',
|
|
|
|
changeType: 'initial',
|
|
|
|
columns: [numCol('col1'), strCol('col2')],
|
|
|
|
},
|
|
|
|
keptLayerIds: ['older'],
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(suggestions).toEqual([]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should suggest unchanged tables when the state is not passed in', () => {
|
|
|
|
const suggestions = datatableVisualization.getSuggestions({
|
|
|
|
table: {
|
|
|
|
isMultiRow: true,
|
|
|
|
layerId: 'first',
|
|
|
|
changeType: 'unchanged',
|
|
|
|
columns: [numCol('col1')],
|
|
|
|
},
|
|
|
|
keptLayerIds: ['first'],
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(suggestions.length).toBeGreaterThan(0);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
describe('#getConfiguration', () => {
|
|
|
|
it('returns a single layer option', () => {
|
|
|
|
const datasource = createMockDatasource('test');
|
2019-09-17 20:57:53 +02:00
|
|
|
const frame = mockFrame();
|
2020-03-17 14:57:52 +01:00
|
|
|
frame.datasourceLayers = { first: datasource.publicAPIMock };
|
2019-09-17 20:57:53 +02:00
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
expect(
|
|
|
|
datatableVisualization.getConfiguration({
|
|
|
|
layerId: 'first',
|
|
|
|
state: {
|
|
|
|
layers: [{ layerId: 'first', columns: [] }],
|
|
|
|
},
|
|
|
|
frame,
|
|
|
|
}).groups
|
2020-09-04 16:11:41 +02:00
|
|
|
).toHaveLength(2);
|
2020-03-17 14:57:52 +01:00
|
|
|
});
|
2019-09-17 20:57:53 +02:00
|
|
|
|
2020-09-04 16:11:41 +02:00
|
|
|
it('allows only bucket operations one category', () => {
|
2020-03-17 14:57:52 +01:00
|
|
|
const datasource = createMockDatasource('test');
|
|
|
|
const frame = mockFrame();
|
|
|
|
frame.datasourceLayers = { first: datasource.publicAPIMock };
|
2019-09-17 20:57:53 +02:00
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
const filterOperations = datatableVisualization.getConfiguration({
|
|
|
|
layerId: 'first',
|
|
|
|
state: {
|
|
|
|
layers: [{ layerId: 'first', columns: [] }],
|
|
|
|
},
|
|
|
|
frame,
|
|
|
|
}).groups[0].filterOperations;
|
2019-09-17 20:57:53 +02:00
|
|
|
|
|
|
|
const baseOperation: Operation = {
|
|
|
|
dataType: 'string',
|
|
|
|
isBucketed: true,
|
|
|
|
label: '',
|
|
|
|
};
|
|
|
|
expect(filterOperations({ ...baseOperation })).toEqual(true);
|
|
|
|
expect(filterOperations({ ...baseOperation, dataType: 'number' })).toEqual(true);
|
|
|
|
expect(filterOperations({ ...baseOperation, dataType: 'date' })).toEqual(true);
|
|
|
|
expect(filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(true);
|
|
|
|
expect(filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual(true);
|
|
|
|
expect(filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false })).toEqual(
|
2020-09-04 16:11:41 +02:00
|
|
|
false
|
|
|
|
);
|
|
|
|
expect(filterOperations({ ...baseOperation, dataType: 'number', isBucketed: false })).toEqual(
|
|
|
|
false
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('allows only metric operations in one category', () => {
|
|
|
|
const datasource = createMockDatasource('test');
|
|
|
|
const frame = mockFrame();
|
|
|
|
frame.datasourceLayers = { first: datasource.publicAPIMock };
|
|
|
|
|
|
|
|
const filterOperations = datatableVisualization.getConfiguration({
|
|
|
|
layerId: 'first',
|
|
|
|
state: {
|
|
|
|
layers: [{ layerId: 'first', columns: [] }],
|
|
|
|
},
|
|
|
|
frame,
|
|
|
|
}).groups[1].filterOperations;
|
|
|
|
|
|
|
|
const baseOperation: Operation = {
|
|
|
|
dataType: 'string',
|
|
|
|
isBucketed: true,
|
|
|
|
label: '',
|
|
|
|
};
|
|
|
|
expect(filterOperations({ ...baseOperation })).toEqual(false);
|
|
|
|
expect(filterOperations({ ...baseOperation, dataType: 'number' })).toEqual(false);
|
|
|
|
expect(filterOperations({ ...baseOperation, dataType: 'date' })).toEqual(false);
|
|
|
|
expect(filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(false);
|
|
|
|
expect(filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual(false);
|
|
|
|
expect(filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false })).toEqual(
|
|
|
|
true
|
|
|
|
);
|
|
|
|
expect(filterOperations({ ...baseOperation, dataType: 'number', isBucketed: false })).toEqual(
|
2019-09-17 20:57:53 +02:00
|
|
|
true
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
it('reorders the rendered colums based on the order from the datasource', () => {
|
|
|
|
const datasource = createMockDatasource('test');
|
2019-09-17 20:57:53 +02:00
|
|
|
const layer = { layerId: 'a', columns: ['b', 'c'] };
|
|
|
|
const frame = mockFrame();
|
|
|
|
frame.datasourceLayers = { a: datasource.publicAPIMock };
|
2020-03-17 14:57:52 +01:00
|
|
|
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
|
|
|
|
|
|
|
|
expect(
|
|
|
|
datatableVisualization.getConfiguration({
|
|
|
|
layerId: 'a',
|
|
|
|
state: { layers: [layer] },
|
|
|
|
frame,
|
2020-09-04 16:11:41 +02:00
|
|
|
}).groups[1].accessors
|
2020-11-17 10:19:13 +01:00
|
|
|
).toEqual([{ columnId: 'c' }, { columnId: 'b' }]);
|
2020-03-17 14:57:52 +01:00
|
|
|
});
|
|
|
|
});
|
2019-09-17 20:57:53 +02:00
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
describe('#removeDimension', () => {
|
|
|
|
it('allows columns to be removed', () => {
|
|
|
|
const layer = { layerId: 'layer1', columns: ['b', 'c'] };
|
|
|
|
expect(
|
|
|
|
datatableVisualization.removeDimension({
|
|
|
|
prevState: { layers: [layer] },
|
|
|
|
layerId: 'layer1',
|
|
|
|
columnId: 'b',
|
|
|
|
})
|
|
|
|
).toEqual({
|
2019-09-17 20:57:53 +02:00
|
|
|
layers: [
|
|
|
|
{
|
2020-03-17 14:57:52 +01:00
|
|
|
layerId: 'layer1',
|
2019-09-17 20:57:53 +02:00
|
|
|
columns: ['c'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
});
|
2020-03-17 14:57:52 +01:00
|
|
|
});
|
2019-09-17 20:57:53 +02:00
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
describe('#setDimension', () => {
|
2019-09-17 20:57:53 +02:00
|
|
|
it('allows columns to be added', () => {
|
2020-03-17 14:57:52 +01:00
|
|
|
const layer = { layerId: 'layer1', columns: ['b', 'c'] };
|
|
|
|
expect(
|
|
|
|
datatableVisualization.setDimension({
|
|
|
|
prevState: { layers: [layer] },
|
|
|
|
layerId: 'layer1',
|
|
|
|
columnId: 'd',
|
|
|
|
groupId: '',
|
|
|
|
})
|
|
|
|
).toEqual({
|
2019-09-17 20:57:53 +02:00
|
|
|
layers: [
|
|
|
|
{
|
2020-03-17 14:57:52 +01:00
|
|
|
layerId: 'layer1',
|
2019-09-17 20:57:53 +02:00
|
|
|
columns: ['b', 'c', 'd'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
});
|
2019-10-16 06:42:58 +02:00
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
it('does not set a duplicate dimension', () => {
|
|
|
|
const layer = { layerId: 'layer1', columns: ['b', 'c'] };
|
|
|
|
expect(
|
|
|
|
datatableVisualization.setDimension({
|
|
|
|
prevState: { layers: [layer] },
|
|
|
|
layerId: 'layer1',
|
|
|
|
columnId: 'b',
|
|
|
|
groupId: '',
|
|
|
|
})
|
|
|
|
).toEqual({
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'layer1',
|
|
|
|
columns: ['b', 'c'],
|
|
|
|
},
|
|
|
|
],
|
2019-10-16 06:42:58 +02:00
|
|
|
});
|
|
|
|
});
|
2019-09-17 20:57:53 +02:00
|
|
|
});
|
2020-08-14 16:54:43 +02:00
|
|
|
|
|
|
|
describe('#toExpression', () => {
|
|
|
|
it('reorders the rendered colums based on the order from the datasource', () => {
|
|
|
|
const datasource = createMockDatasource('test');
|
|
|
|
const layer = { layerId: 'a', columns: ['b', 'c'] };
|
|
|
|
const frame = mockFrame();
|
|
|
|
frame.datasourceLayers = { a: datasource.publicAPIMock };
|
|
|
|
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
|
|
|
|
datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
|
|
|
|
dataType: 'string',
|
2020-11-04 18:28:00 +01:00
|
|
|
isBucketed: false, // <= make them metrics
|
2020-08-14 16:54:43 +02:00
|
|
|
label: 'label',
|
|
|
|
});
|
|
|
|
|
2020-08-21 18:08:25 +02:00
|
|
|
const expression = datatableVisualization.toExpression(
|
|
|
|
{ layers: [layer] },
|
|
|
|
frame.datasourceLayers
|
|
|
|
) as Ast;
|
2020-11-04 18:28:00 +01:00
|
|
|
|
2020-08-14 16:54:43 +02:00
|
|
|
const tableArgs = buildExpression(expression).findFunction('lens_datatable_columns');
|
|
|
|
|
|
|
|
expect(tableArgs).toHaveLength(1);
|
|
|
|
expect(tableArgs[0].arguments).toEqual({
|
|
|
|
columnIds: ['c', 'b'],
|
|
|
|
});
|
|
|
|
});
|
2020-11-04 18:28:00 +01:00
|
|
|
|
|
|
|
it('returns no expression if the metric dimension is not defined', () => {
|
|
|
|
const datasource = createMockDatasource('test');
|
|
|
|
const layer = { layerId: 'a', columns: ['b', 'c'] };
|
|
|
|
const frame = mockFrame();
|
|
|
|
frame.datasourceLayers = { a: datasource.publicAPIMock };
|
|
|
|
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
|
|
|
|
datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
|
|
|
|
dataType: 'string',
|
|
|
|
isBucketed: true, // move it from the metric to the break down by side
|
|
|
|
label: 'label',
|
|
|
|
});
|
|
|
|
|
|
|
|
const expression = datatableVisualization.toExpression(
|
|
|
|
{ layers: [layer] },
|
|
|
|
frame.datasourceLayers
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(expression).toEqual(null);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('#getErrorMessages', () => {
|
|
|
|
it('returns undefined if the datasource is missing a metric dimension', () => {
|
|
|
|
const datasource = createMockDatasource('test');
|
|
|
|
const layer = { layerId: 'a', columns: ['b', 'c'] };
|
|
|
|
const frame = mockFrame();
|
|
|
|
frame.datasourceLayers = { a: datasource.publicAPIMock };
|
|
|
|
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
|
|
|
|
datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
|
|
|
|
dataType: 'string',
|
|
|
|
isBucketed: true, // move it from the metric to the break down by side
|
|
|
|
label: 'label',
|
|
|
|
});
|
|
|
|
|
|
|
|
const error = datatableVisualization.getErrorMessages({ layers: [layer] }, frame);
|
|
|
|
|
2020-11-20 19:42:12 +01:00
|
|
|
expect(error).toBeUndefined();
|
2020-11-04 18:28:00 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('returns undefined if the metric dimension is defined', () => {
|
|
|
|
const datasource = createMockDatasource('test');
|
|
|
|
const layer = { layerId: 'a', columns: ['b', 'c'] };
|
|
|
|
const frame = mockFrame();
|
|
|
|
frame.datasourceLayers = { a: datasource.publicAPIMock };
|
|
|
|
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
|
|
|
|
datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
|
|
|
|
dataType: 'string',
|
|
|
|
isBucketed: false, // keep it a metric
|
|
|
|
label: 'label',
|
|
|
|
});
|
|
|
|
|
|
|
|
const error = datatableVisualization.getErrorMessages({ layers: [layer] }, frame);
|
|
|
|
|
2020-11-20 19:42:12 +01:00
|
|
|
expect(error).toBeUndefined();
|
2020-11-04 18:28:00 +01:00
|
|
|
});
|
2020-08-14 16:54:43 +02:00
|
|
|
});
|
2019-09-17 20:57:53 +02:00
|
|
|
});
|