[lens] Index pattern suggest on drop

This commit is contained in:
Wylie Conlon 2019-06-12 18:16:04 -04:00
parent 9ea8b9a041
commit 604c6ed68c
7 changed files with 428 additions and 19 deletions

View file

@ -21,7 +21,7 @@ import { ReactWrapper } from 'enzyme';
const waitForPromises = () => new Promise(resolve => setTimeout(resolve));
describe('workspace_panel', () => {
let mockVisualization: Visualization;
let mockVisualization: jest.Mocked<Visualization>;
let mockDatasource: DatasourceMock;
let expressionRendererMock: jest.Mock<React.ReactElement, [ExpressionRendererProps]>;
@ -274,4 +274,136 @@ Object {
expect(instance.find(expressionRendererMock).length).toBe(1);
});
});
describe('suggestions from dropping in workspace panel', () => {
let mockDispatch: jest.Mock;
beforeEach(() => {
mockDispatch = jest.fn();
instance = mount(
<WorkspacePanel
activeDatasource={mockDatasource}
datasourceState={{}}
activeVisualizationId={null}
visualizationMap={{
vis: mockVisualization,
}}
visualizationState={{}}
datasourcePublicAPI={mockDatasource.publicAPIMock}
dispatch={mockDispatch}
ExpressionRenderer={expressionRendererMock}
/>
);
});
it('should immediately transition if exactly one suggestion is returned', () => {
const expectedTable = {
datasourceSuggestionId: 0,
isMultiRow: true,
columns: [],
};
mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([
{
state: {},
table: expectedTable,
},
]);
mockVisualization.getSuggestions.mockReturnValueOnce([
{
score: 0.5,
title: 'my title',
state: {},
datasourceSuggestionId: 0,
},
]);
instance.childAt(0).prop('onDrop')({
name: '@timestamp',
type: 'date',
searchable: false,
aggregatable: false,
});
expect(mockDatasource.getDatasourceSuggestionsForField).toHaveBeenCalledTimes(1);
expect(mockVisualization.getSuggestions).toHaveBeenCalledWith(
expect.objectContaining({
tables: [expectedTable],
})
);
expect(mockDispatch).toHaveBeenCalledWith({
type: 'SWITCH_VISUALIZATION',
newVisualizationId: 'vis',
initialState: {},
datasourceState: {},
});
});
it('should immediately transition to the first suggestion if there are multiple', () => {
mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([
{
state: {},
table: {
datasourceSuggestionId: 0,
isMultiRow: true,
columns: [],
},
},
{
state: {},
table: {
datasourceSuggestionId: 1,
isMultiRow: true,
columns: [],
},
},
]);
mockVisualization.getSuggestions.mockReturnValueOnce([
{
score: 0.8,
title: 'first suggestion',
state: {
isFirst: true,
},
datasourceSuggestionId: 1,
},
{
score: 0.5,
title: 'second suggestion',
state: {},
datasourceSuggestionId: 0,
},
]);
instance.childAt(0).prop('onDrop')({
name: '@timestamp',
type: 'date',
searchable: false,
aggregatable: false,
});
expect(mockDispatch).toHaveBeenCalledWith({
type: 'SWITCH_VISUALIZATION',
newVisualizationId: 'vis',
initialState: {
isFirst: true,
},
datasourceState: {},
});
});
it("should do nothing when the visualization can't use the suggestions", () => {
mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([]);
instance.childAt(0).prop('onDrop')({
name: '@timestamp',
type: 'date',
searchable: false,
aggregatable: false,
});
expect(mockDatasource.getDatasourceSuggestionsForField).toHaveBeenCalledTimes(1);
expect(mockVisualization.getSuggestions).toHaveBeenCalledTimes(1);
expect(mockDispatch).not.toHaveBeenCalled();
});
});
});

View file

@ -37,9 +37,10 @@ export function WorkspacePanel({
dispatch,
ExpressionRenderer: ExpressionRendererComponent,
}: WorkspacePanelProps) {
function onDrop() {
function onDrop(item: unknown) {
const datasourceSuggestions = activeDatasource.getDatasourceSuggestionsForField(
datasourceState
datasourceState,
item
);
const suggestions = getSuggestions(

View file

@ -35,7 +35,7 @@ export function createMockDatasource(): DatasourceMock {
};
return {
getDatasourceSuggestionsForField: jest.fn(_state => []),
getDatasourceSuggestionsForField: jest.fn((_state, item) => []),
getDatasourceSuggestionsFromCurrentState: jest.fn(_state => []),
getPersistableState: jest.fn(),
getPublicAPI: jest.fn((_state, _setState) => publicAPIMock),

View file

@ -279,6 +279,175 @@ describe('IndexPattern Data Source', () => {
});
});
describe('#getDatasourceSuggestionsForField', () => {
describe('with no previous selections', () => {
let initialState: IndexPatternPrivateState;
beforeEach(async () => {
initialState = await indexPatternDatasource.initialize({
currentIndexPatternId: '1',
columnOrder: [],
columns: {},
});
});
it('should apply a bucketed aggregation for a string field', () => {
const suggestions = indexPatternDatasource.getDatasourceSuggestionsForField(initialState, {
name: 'source',
type: 'string',
aggregatable: true,
searchable: true,
});
expect(suggestions).toHaveLength(1);
expect(suggestions[0].state).toEqual(
expect.objectContaining({
columnOrder: ['col1', 'col2'],
columns: {
col1: expect.objectContaining({
operationType: 'terms',
sourceField: 'source',
}),
col2: expect.objectContaining({
operationType: 'count',
sourceField: 'documents',
}),
},
})
);
expect(suggestions[0].table).toEqual({
datasourceSuggestionId: 0,
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'col1',
}),
expect.objectContaining({
columnId: 'col2',
}),
],
});
});
it('should apply a bucketed aggregation for a date field', () => {
const suggestions = indexPatternDatasource.getDatasourceSuggestionsForField(initialState, {
name: 'timestamp',
type: 'date',
aggregatable: true,
searchable: true,
});
expect(suggestions).toHaveLength(1);
expect(suggestions[0].state).toEqual(
expect.objectContaining({
columnOrder: ['col1', 'col2'],
columns: {
col1: expect.objectContaining({
operationType: 'date_histogram',
sourceField: 'timestamp',
}),
col2: expect.objectContaining({
operationType: 'count',
sourceField: 'documents',
}),
},
})
);
expect(suggestions[0].table).toEqual({
datasourceSuggestionId: 0,
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'col1',
}),
expect.objectContaining({
columnId: 'col2',
}),
],
});
});
it('should select a metric for a number field', () => {
const suggestions = indexPatternDatasource.getDatasourceSuggestionsForField(initialState, {
name: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
});
expect(suggestions).toHaveLength(1);
expect(suggestions[0].state).toEqual(
expect.objectContaining({
columnOrder: ['col1', 'col2'],
columns: {
col1: expect.objectContaining({
sourceField: 'timestamp',
operationType: 'date_histogram',
}),
col2: expect.objectContaining({
sourceField: 'bytes',
operationType: 'sum',
}),
},
})
);
expect(suggestions[0].table).toEqual({
datasourceSuggestionId: 0,
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'col1',
}),
expect.objectContaining({
columnId: 'col2',
}),
],
});
});
});
describe('with a prior column', () => {
let initialState: IndexPatternPrivateState;
beforeEach(async () => {
initialState = await indexPatternDatasource.initialize(persistedState);
});
it('should not suggest for string', () => {
expect(
indexPatternDatasource.getDatasourceSuggestionsForField(initialState, {
name: 'source',
type: 'string',
aggregatable: true,
searchable: true,
})
).toHaveLength(0);
});
it('should not suggest for date', () => {
expect(
indexPatternDatasource.getDatasourceSuggestionsForField(initialState, {
name: 'timestamp',
type: 'date',
aggregatable: true,
searchable: true,
})
).toHaveLength(0);
});
it('should not suggest for number', () => {
expect(
indexPatternDatasource.getDatasourceSuggestionsForField(initialState, {
name: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
})
).toHaveLength(0);
});
});
});
describe('#getPublicAPI', () => {
let publicAPI: DatasourcePublicAPI;

View file

@ -7,6 +7,7 @@
import _ from 'lodash';
import React from 'react';
import { render } from 'react-dom';
import { i18n } from '@kbn/i18n';
import { Chrome } from 'ui/chrome';
import { ToastNotifications } from 'ui/notify/toasts/toast_notifications';
import { EuiComboBox } from '@elastic/eui';
@ -16,11 +17,13 @@ import {
DatasourceDimensionPanelProps,
DatasourceDataPanelProps,
DimensionPriority,
DatasourceSuggestion,
} from '../types';
import { getIndexPatterns } from './loader';
import { ChildDragDropProvider, DragDrop } from '../drag_drop';
import { toExpression } from './to_expression';
import { IndexPatternDimensionPanel } from './dimension_panel';
import { makeOperation, getOperationTypesForField } from './operations';
export type OperationType =
| 'value'
@ -243,11 +246,105 @@ export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: To
};
},
getDatasourceSuggestionsForField() {
getDatasourceSuggestionsForField(
state,
item
): Array<DatasourceSuggestion<IndexPatternPrivateState>> {
const field: IndexPatternField = item as IndexPatternField;
if (Object.keys(state.columns).length) {
// Not sure how to suggest multiple fields yet
return [];
}
const operations = getOperationTypesForField(field);
const hasBucket = operations.find(op => op === 'date_histogram' || op === 'terms');
if (hasBucket) {
const column = makeOperation(0, hasBucket, field);
const countColumn: IndexPatternColumn = {
operationId: 'count',
label: i18n.translate('xpack.lens.indexPatternOperations.countOfDocuments', {
defaultMessage: 'Count of Documents',
}),
dataType: 'number',
isBucketed: false,
operationType: 'count',
sourceField: 'documents',
};
const suggestion: DatasourceSuggestion<IndexPatternPrivateState> = {
state: {
...state,
columns: {
col1: column,
col2: countColumn,
},
columnOrder: ['col1', 'col2'],
},
table: {
columns: [
{
columnId: 'col1',
operation: columnToOperation(column),
},
{
columnId: 'col2',
operation: columnToOperation(countColumn),
},
],
isMultiRow: true,
datasourceSuggestionId: 0,
},
};
return [suggestion];
} else if (state.indexPatterns[state.currentIndexPatternId].timeFieldName) {
const currentIndexPattern = state.indexPatterns[state.currentIndexPatternId];
const dateField = currentIndexPattern.fields.find(
f => f.name === currentIndexPattern.timeFieldName
)!;
const column = makeOperation(0, operations[0], field);
const dateColumn = makeOperation(1, 'date_histogram', dateField);
const suggestion: DatasourceSuggestion<IndexPatternPrivateState> = {
state: {
...state,
columns: {
col1: dateColumn,
col2: column,
},
columnOrder: ['col1', 'col2'],
},
table: {
columns: [
{
columnId: 'col1',
operation: columnToOperation(column),
},
{
columnId: 'col2',
operation: columnToOperation(dateColumn),
},
],
isMultiRow: true,
datasourceSuggestionId: 0,
},
};
return [suggestion];
}
return [];
},
getDatasourceSuggestionsFromCurrentState() {
getDatasourceSuggestionsFromCurrentState(state) {
return [];
},
};

View file

@ -159,28 +159,38 @@ export function getOperationResultType({ type }: IndexPatternField, op: Operatio
}
}
export function makeOperation(
index: number,
op: OperationType,
field: IndexPatternField,
suggestedOrder?: DimensionPriority
): IndexPatternColumn {
const operationPanels = getOperationDisplay();
return {
operationId: `${index}${op}`,
label: operationPanels[op].ofName(field.name),
dataType: getOperationResultType(field, op),
isBucketed: op === 'terms' || op === 'date_histogram',
operationType: op,
sourceField: field.name,
suggestedOrder,
};
}
export function getPotentialColumns(
state: IndexPatternPrivateState,
suggestedOrder?: DimensionPriority
): IndexPatternColumn[] {
const fields = state.indexPatterns[state.currentIndexPatternId].fields;
const operationPanels = getOperationDisplay();
const columns: IndexPatternColumn[] = fields
.map((field, index) => {
const validOperations = getOperationTypesForField(field);
return validOperations.map(op => ({
operationId: `${index}${op}`,
label: operationPanels[op].ofName(field.name),
dataType: getOperationResultType(field, op),
isBucketed: op === 'terms' || op === 'date_histogram',
operationType: op,
sourceField: field.name,
suggestedOrder,
}));
return validOperations.map(op => {
return makeOperation(index, op, field, suggestedOrder);
});
})
.reduce((prev, current) => prev.concat(current));

View file

@ -57,7 +57,7 @@ export interface Datasource<T = unknown, P = unknown> {
toExpression: (state: T) => Ast | string | null;
getDatasourceSuggestionsForField: (state: T) => Array<DatasourceSuggestion<T>>;
getDatasourceSuggestionsForField: (state: T, field: unknown) => Array<DatasourceSuggestion<T>>;
getDatasourceSuggestionsFromCurrentState: (state: T) => Array<DatasourceSuggestion<T>>;
getPublicAPI: (state: T, setState: (newState: T) => void) => DatasourcePublicAPI;