mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[lens] Index pattern suggest on drop
This commit is contained in:
parent
9ea8b9a041
commit
604c6ed68c
7 changed files with 428 additions and 19 deletions
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 [];
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue