From f46855ecd7f26794a0c17dcdfc83db09fc0f0c50 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 30 May 2019 18:41:39 -0400 Subject: [PATCH 01/27] [lens] Dimension panel lets users select operations and fields individually --- .../__snapshots__/indexpattern.test.tsx.snap | 204 +++++++++++++-- .../indexpattern_plugin/indexpattern.test.tsx | 43 +++- .../indexpattern_plugin/indexpattern.tsx | 241 ++++++++++++++---- 3 files changed, 406 insertions(+), 82 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/indexpattern.test.tsx.snap b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/indexpattern.test.tsx.snap index 45760f0bf5efb..9fa213f496b7b 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/indexpattern.test.tsx.snap +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/indexpattern.test.tsx.snap @@ -3,35 +3,183 @@ exports[`IndexPattern Data Source #getPublicAPI renderDimensionPanel should render a dimension panel 1`] = `
Dimension Panel - + + + Configure dimension + + } - } - /> + closePopover={[Function]} + hasArrow={true} + id="col2" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + + +
+ + + value + + + + + terms + + + + + date_histogram + + + + + sum + + + + + average + + + + + count + + +
+
+ + + +
+
+
`; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index 418cb9eab2443..9a575686a541c 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -178,7 +178,7 @@ describe('IndexPattern Data Source', () => { // Private operationType: 'value', - sourceField: 'op', + sourceField: 'source', }, col2: { operationId: 'op2', @@ -188,13 +188,46 @@ describe('IndexPattern Data Source', () => { // Private operationType: 'value', - sourceField: 'op2', + sourceField: 'bytes', }, }, }; const state = await indexPatternDatasource.initialize(queryPersistedState); expect(indexPatternDatasource.toExpression(state)).toMatchInlineSnapshot( - `"esdocs index=\\"1\\" fields=\\"op, op2\\" sort=\\"op, DESC\\""` + `"esdocs index=\\"1\\" fields=\\"source, bytes\\" sort=\\"source, DESC\\""` + ); + }); + + it('should generate an expression for an aggregated query', async () => { + const queryPersistedState: IndexPatternPersistedState = { + currentIndexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + operationId: 'op1', + label: 'Count of Documents', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + sourceField: 'document', + }, + col2: { + operationId: 'op2', + label: 'Date', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + }, + }, + }; + const state = await indexPatternDatasource.initialize(queryPersistedState); + expect(indexPatternDatasource.toExpression(state)).toMatchInlineSnapshot( + `"esaggs index=\\"1\\" aggs=\\"{\\"timestamp\\":{\\"date_histogram\\":{\\"field\\":\\"timestamp\\",\\"aggs\\":{\\"document\\":{\\"count\\":{\\"field\\":\\"document\\"}}}}}}\\""` ); }); }); @@ -260,7 +293,7 @@ describe('IndexPattern Data Source', () => { /> ); - expect(filterOperations).toBeCalledTimes(3); + expect(filterOperations).toBeCalled(); }); it('should filter out all selections if the filter returns false', () => { @@ -299,7 +332,7 @@ describe('IndexPattern Data Source', () => { ...state.columns, col2: { operationId: firstOption.value, - label: 'Value of timestamp', + label: 'value of timestamp', dataType: 'date', isBucketed: false, operationType: 'value', diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index ec558609e3900..35d26d2482d13 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -4,16 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import _ from 'lodash'; +import React, { useState } from 'react'; import { render } from 'react-dom'; import { Chrome } from 'ui/chrome'; import { ToastNotifications } from 'ui/notify/toasts/toast_notifications'; -import { EuiComboBox } from '@elastic/eui'; +import { EuiComboBox, EuiPopover, EuiButton, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { Datasource, DataType } from '..'; import { DatasourceDimensionPanelProps, DatasourceDataPanelProps } from '../types'; import { getIndexPatterns } from './loader'; -type OperationType = 'value' | 'terms' | 'date_histogram'; +type OperationType = 'value' | 'terms' | 'date_histogram' | 'sum' | 'average' | 'count'; + +const operations: OperationType[] = ['value', 'terms', 'date_histogram', 'sum', 'average', 'count']; interface IndexPatternColumn { // Public @@ -99,17 +102,63 @@ export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { setState: (newState: IndexPatternPrivateState) => void; }; +function getOperationTypesForField({ type }: IndexPatternField): OperationType[] { + switch (type) { + case 'date': + return ['value', 'date_histogram']; + case 'number': + return ['value', 'sum', 'average']; + case 'string': + return ['value', 'terms']; + } + return []; +} + +function getOperationResultType({ type }: IndexPatternField, op: OperationType): DataType { + switch (op) { + case 'value': + return type as DataType; + case 'average': + case 'count': + case 'sum': + return 'number'; + case 'date_histogram': + return 'date'; + case 'terms': + return 'string'; + } +} + export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) { + const [isOpen, setOpen] = useState(false); + const fields = props.state.indexPatterns[props.state.currentIndexPatternId].fields; - const columns: IndexPatternColumn[] = fields.map((field, index) => ({ - operationId: `${index}`, - label: `Value of ${field.name}`, - dataType: field.type as DataType, + + const columns: IndexPatternColumn[] = fields + .map((field, index) => { + const validOperations = getOperationTypesForField(field); + + return validOperations.map(op => ({ + operationId: `${index}${op}`, + label: `${op} of ${field.name}`, + dataType: getOperationResultType(field, op), + isBucketed: op !== 'terms' && op !== 'date_histogram', + + operationType: op, + sourceField: field.name, + })); + }) + .reduce((prev, current) => prev.concat(current)); + + columns.push({ + operationId: 'count', + label: 'Count of Documents', + dataType: 'number', isBucketed: false, - operationType: 'value' as OperationType, - sourceField: field.name, - })); + operationType: 'count', + sourceField: 'documents', + }); const filteredColumns = columns.filter(col => { const { operationId, label, dataType, isBucketed } = col; @@ -124,44 +173,110 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp const selectedColumn: IndexPatternColumn | null = props.state.columns[props.columnId] || null; + const uniqueColumns = _.uniq(filteredColumns, col => col.operationType); + + const columnsFromFunction = selectedColumn + ? filteredColumns.filter(col => { + return col.operationType === selectedColumn.operationType; + }) + : filteredColumns; + return (
Dimension Panel - ({ - label: col.label, - value: col.operationId, - }))} - selectedOptions={ - selectedColumn - ? [ - { - label: selectedColumn.label, - value: selectedColumn.operationId, - }, - ] - : [] - } - singleSelection={{ asPlainText: true }} - isClearable={false} - onChange={choices => { - const column: IndexPatternColumn = columns.find( - ({ operationId }) => operationId === choices[0].value - )!; - const newColumns: IndexPatternPrivateState['columns'] = { - ...props.state.columns, - [props.columnId]: column, - }; - - props.setState({ - ...props.state, - columns: newColumns, - // Order is not meaningful until we aggregate - columnOrder: Object.keys(newColumns), - }); - }} - /> + + { + setOpen(false); + }} + ownFocus + anchorPosition="rightCenter" + button={ + { + setOpen(!isOpen); + }} + > + {selectedColumn ? selectedColumn.label : 'Configure dimension'} + + } + > + + +
+ {operations.map(o => ( + col.operationType === o)} + onClick={() => { + const newColumn: IndexPatternColumn = uniqueColumns.find( + col => col.operationType === o + )!; + + props.setState({ + ...props.state, + columnOrder: _.uniq( + Object.keys(props.state.columns).concat(props.columnId) + ), + columns: { + ...props.state.columns, + [props.columnId]: newColumn, + }, + }); + }} + > + {o} + + ))} +
+
+ + ({ + label: col.sourceField, + value: col.operationId, + }))} + selectedOptions={ + selectedColumn + ? [ + { + label: selectedColumn.sourceField, + value: selectedColumn.operationId, + }, + ] + : [] + } + singleSelection={{ asPlainText: true }} + isClearable={false} + onChange={choices => { + const column: IndexPatternColumn = columns.find( + ({ operationId }) => operationId === choices[0].value + )!; + const newColumns: IndexPatternPrivateState['columns'] = { + ...props.state.columns, + [props.columnId]: column, + }; + + props.setState({ + ...props.state, + columns: newColumns, + // Order is not meaningful until we aggregate + columnOrder: Object.keys(newColumns), + }); + }} + /> + +
+
+
); } @@ -203,11 +318,39 @@ export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: To } const fieldNames = state.columnOrder.map(col => state.columns[col].sourceField); - const expression = `esdocs index="${state.currentIndexPatternId}" fields="${fieldNames.join( - ', ' - )}" sort="${fieldNames[0]}, DESC"`; + const sortedColumns = state.columnOrder.map(col => state.columns[col]); + + if (sortedColumns.every(({ operationType }) => operationType === 'value')) { + return `esdocs index="${state.currentIndexPatternId}" fields="${fieldNames.join( + ', ' + )}" sort="${fieldNames[0]}, DESC"`; + } else if (sortedColumns.length) { + let topAgg: object; + sortedColumns.forEach((col, index) => { + if (topAgg) { + topAgg = { + [fieldNames[index]]: { + [col.operationType]: { + field: col.sourceField, + aggs: topAgg as object, + }, + }, + }; + } else { + topAgg = { + [fieldNames[index]]: { + [col.operationType]: { + field: col.sourceField, + }, + }, + }; + } + }); + + return `esaggs index="${state.currentIndexPatternId}" aggs="${JSON.stringify(topAgg!)}"`; + } - return expression; + return ''; }, renderDataPanel( From 30f69e39d4576428dd5538c54ac33080ad78cd95 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 3 Jun 2019 18:27:38 -0400 Subject: [PATCH 02/27] Split files and add tests --- x-pack/plugins/lens/public/index.scss | 1 + .../dimension_panel.test.tsx.snap | 164 ++++++++++ .../__snapshots__/indexpattern.test.tsx.snap | 183 ----------- .../dimension_panel.test.tsx | 292 +++++++++++++++++ .../indexpattern_plugin/dimension_panel.tsx | 301 ++++++++++++++++++ .../indexpattern_plugin/indexpattern.scss | 3 + .../indexpattern_plugin/indexpattern.test.tsx | 94 +----- .../indexpattern_plugin/indexpattern.tsx | 238 +------------- .../indexpattern_plugin/to_expression.ts | 92 ++++++ 9 files changed, 867 insertions(+), 501 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.scss create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts diff --git a/x-pack/plugins/lens/public/index.scss b/x-pack/plugins/lens/public/index.scss index e69de29bb2d1d..e3208e27af3c0 100644 --- a/x-pack/plugins/lens/public/index.scss +++ b/x-pack/plugins/lens/public/index.scss @@ -0,0 +1 @@ +@import './indexpattern_plugin/indexpattern'; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap new file mode 100644 index 0000000000000..f3e1bda3f5d1f --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap @@ -0,0 +1,164 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IndexPatternDimensionPanel should render a dimension panel 1`] = ` +
+ + + + + Configure dimension + + +
+ } + closePopover={[Function]} + hasArrow={true} + id="col2" + isOpen={false} + ownFocus={true} + panelClassName="lns-indexPattern-dimensionPopover" + panelPaddingSize="m" + > + + + + + +
+ + + Value + + + + + Top Values + + + + + Date Histogram + + + + + Sum + + + + + Average + + + + + Count + + +
+
+
+ + + +`; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/indexpattern.test.tsx.snap b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/indexpattern.test.tsx.snap index 9fa213f496b7b..a0cde1e3f58e1 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/indexpattern.test.tsx.snap +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/indexpattern.test.tsx.snap @@ -1,188 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`IndexPattern Data Source #getPublicAPI renderDimensionPanel should render a dimension panel 1`] = ` -
- Dimension Panel - - - - Configure dimension - - - } - closePopover={[Function]} - hasArrow={true} - id="col2" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - - -
- - - value - - - - - terms - - - - - date_histogram - - - - - sum - - - - - average - - - - - count - - -
-
- - - -
-
-
-
-`; - exports[`IndexPattern Data Source #renderDataPanel should match snapshot 1`] = `
Index Pattern Data Source diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx new file mode 100644 index 0000000000000..a283449009070 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx @@ -0,0 +1,292 @@ +/* + * 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 { mount, shallow } from 'enzyme'; +import React from 'react'; +import { EuiComboBox } from '@elastic/eui'; +import { IndexPatternPrivateState } from './indexpattern'; +import { getPotentialColumns, IndexPatternDimensionPanel } from './dimension_panel'; + +const expectedIndexPatterns = { + 1: { + id: '1', + title: 'Fake Index Pattern', + timeFieldName: 'timestamp', + fields: [ + { + name: 'timestamp', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + ], + }, + 2: { + id: '2', + title: 'Fake Rollup Pattern', + timeFieldName: 'timestamp', + fields: [ + { + name: 'timestamp', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + ], + }, +}; + +describe('IndexPatternDimensionPanel', () => { + let state: IndexPatternPrivateState; + + beforeEach(() => { + state = { + indexPatterns: expectedIndexPatterns, + currentIndexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'Value of timestamp', + dataType: 'string', + isBucketed: false, + + // Private + operationType: 'value', + sourceField: 'timestamp', + }, + }, + }; + }); + + describe('getPotentialColumns', () => { + it('should list operations by field', () => { + const columns = getPotentialColumns(state); + + expect(columns.map(col => [col.sourceField, col.operationType])).toMatchInlineSnapshot(` +Array [ + Array [ + "timestamp", + "value", + ], + Array [ + "timestamp", + "date_histogram", + ], + Array [ + "bytes", + "value", + ], + Array [ + "bytes", + "sum", + ], + Array [ + "bytes", + "average", + ], + Array [ + "source", + "value", + ], + Array [ + "source", + "terms", + ], + Array [ + "documents", + "count", + ], +] +`); + }); + }); + + it('should render a dimension panel', () => { + const wrapper = shallow( + {}} + columnId={'col2'} + filterOperations={() => true} + /> + ); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should display a call to action in the popover button', () => { + const wrapper = mount( + {}} + columnId={'col2'} + filterOperations={() => true} + /> + ); + expect( + wrapper + .find('[data-test-subj="indexPattern-dimensionPopover-button"]') + .first() + .text() + ).toEqual('Configure dimension'); + }); + + it('should call the filterOperations function', () => { + const filterOperations = jest.fn().mockReturnValue(true); + + shallow( + {}} + columnId={'col2'} + filterOperations={filterOperations} + /> + ); + + expect(filterOperations).toBeCalled(); + }); + + it('should not show any choices if the filter returns false', () => { + const wrapper = shallow( + {}} + columnId={'col2'} + filterOperations={() => false} + /> + ); + + expect(wrapper.find(EuiComboBox)!.prop('options')!.length).toEqual(0); + }); + + it("should disable functions that won't work with dates", () => { + const setState = jest.fn(); + + const wrapper = shallow( + true} + /> + ); + + expect( + wrapper.find('[data-test-subj="lns-indexPatternDimension-value"]').prop('color') + ).toEqual('primary'); + expect( + wrapper.find('[data-test-subj="lns-indexPatternDimension-value"]').prop('isDisabled') + ).toEqual(false); + expect( + wrapper.find('[data-test-subj="lns-indexPatternDimension-terms"]').prop('isDisabled') + ).toEqual(true); + expect( + wrapper.find('[data-test-subj="lns-indexPatternDimension-date_histogram"]').prop('isDisabled') + ).toEqual(false); + expect( + wrapper.find('[data-test-subj="lns-indexPatternDimension-sum"]').prop('isDisabled') + ).toEqual(true); + expect( + wrapper.find('[data-test-subj="lns-indexPatternDimension-average"]').prop('isDisabled') + ).toEqual(true); + expect( + wrapper.find('[data-test-subj="lns-indexPatternDimension-count"]').prop('isDisabled') + ).toEqual(true); + }); + + it('should update the datasource state on selection of a value operation', () => { + const setState = jest.fn(); + + const wrapper = shallow( + true} + /> + ); + + const comboBox = wrapper.find(EuiComboBox)!; + const firstOption = comboBox.prop('options')![0]; + + comboBox.prop('onChange')!([firstOption]); + + expect(setState).toHaveBeenCalledWith({ + ...state, + columns: { + ...state.columns, + col2: { + operationId: firstOption.value, + label: 'Value of timestamp', + dataType: 'date', + isBucketed: false, + operationType: 'value', + sourceField: 'timestamp', + }, + }, + columnOrder: ['col1', 'col2'], + }); + }); + + it('should update the datasource state when the user makes a selection', () => { + const setState = jest.fn(); + + const wrapper = shallow( + op.dataType === 'number'} + /> + ); + + const comboBox = wrapper.find(EuiComboBox)!; + const firstField = comboBox.prop('options')![0]; + + comboBox.prop('onChange')!([firstField]); + + expect(setState).toHaveBeenCalledWith({ + ...state, + columns: { + ...state.columns, + col2: { + operationId: firstField.value, + label: 'Value of bytes', + dataType: 'number', + isBucketed: false, + operationType: 'value', + sourceField: 'bytes', + }, + }, + columnOrder: ['col1', 'col2'], + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx new file mode 100644 index 0000000000000..dab84bef16690 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx @@ -0,0 +1,301 @@ +/* + * 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 _ from 'lodash'; +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiComboBox, EuiPopover, EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { DataType, DatasourceDimensionPanelProps } from '../types'; +import { + IndexPatternColumn, + IndexPatternField, + IndexPatternPrivateState, + OperationType, +} from './indexpattern'; + +const operations: OperationType[] = ['value', 'terms', 'date_histogram', 'sum', 'average', 'count']; + +const operationPanels: Record< + OperationType, + { + type: OperationType; + displayName: string; + ofName: (name: string) => string; + } +> = { + value: { + type: 'value', + displayName: i18n.translate('xpack.lens.indexPatternOperations.value', { + defaultMessage: 'Value', + }), + ofName: name => + i18n.translate('xpack.lens.indexPatternOperations.valueOf', { + defaultMessage: 'Value of {name}', + values: { name }, + }), + }, + terms: { + type: 'terms', + displayName: i18n.translate('xpack.lens.indexPatternOperations.terms', { + defaultMessage: 'Top Values', + }), + ofName: name => + i18n.translate('xpack.lens.indexPatternOperations.termsOf', { + defaultMessage: 'Top Values of {name}', + values: { name }, + }), + }, + date_histogram: { + type: 'date_histogram', + displayName: i18n.translate('xpack.lens.indexPatternOperations.dateHistogram', { + defaultMessage: 'Date Histogram', + }), + ofName: name => + i18n.translate('xpack.lens.indexPatternOperations.dateHistogramOf', { + defaultMessage: 'Date Histogram of', + values: { name }, + }), + }, + sum: { + type: 'sum', + displayName: i18n.translate('xpack.lens.indexPatternOperations.sum', { + defaultMessage: 'Sum', + }), + ofName: name => + i18n.translate('xpack.lens.indexPatternOperations.sumOf', { + defaultMessage: 'Sum of', + values: { name }, + }), + }, + average: { + type: 'average', + displayName: i18n.translate('xpack.lens.indexPatternOperations.average', { + defaultMessage: 'Average', + }), + ofName: name => + i18n.translate('xpack.lens.indexPatternOperations.averageOf', { + defaultMessage: 'Average of', + values: { name }, + }), + }, + count: { + type: 'count', + displayName: i18n.translate('xpack.lens.indexPatternOperations.count', { + defaultMessage: 'Count', + }), + ofName: name => + i18n.translate('xpack.lens.indexPatternOperations.countOf', { + defaultMessage: 'Count of', + values: { name }, + }), + }, +}; + +export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { + state: IndexPatternPrivateState; + setState: (newState: IndexPatternPrivateState) => void; +}; + +function getOperationTypesForField({ type }: IndexPatternField): OperationType[] { + switch (type) { + case 'date': + return ['value', 'date_histogram']; + case 'number': + return ['value', 'sum', 'average']; + case 'string': + return ['value', 'terms']; + } + return []; +} + +function getOperationResultType({ type }: IndexPatternField, op: OperationType): DataType { + switch (op) { + case 'value': + return type as DataType; + case 'average': + case 'count': + case 'sum': + return 'number'; + case 'date_histogram': + return 'date'; + case 'terms': + return 'string'; + } +} + +export function getPotentialColumns(state: IndexPatternPrivateState): IndexPatternColumn[] { + const fields = state.indexPatterns[state.currentIndexPatternId].fields; + + 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, + })); + }) + .reduce((prev, current) => prev.concat(current)); + + columns.push({ + operationId: 'count', + label: i18n.translate('xpack.lens.indexPatternOperations.countOfDocuments', { + defaultMessage: 'Count of Documents', + }), + dataType: 'number', + isBucketed: false, + + operationType: 'count', + sourceField: 'documents', + }); + + return columns; +} + +export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) { + const [isOpen, setOpen] = useState(false); + + const columns = getPotentialColumns(props.state); + + const filteredColumns = columns.filter(col => { + const { operationId, label, dataType, isBucketed } = col; + + return props.filterOperations({ + id: operationId, + label, + dataType, + isBucketed, + }); + }); + + const selectedColumn: IndexPatternColumn | null = props.state.columns[props.columnId] || null; + + const uniqueColumnsByField = _.uniq(filteredColumns, col => col.sourceField); + + const functionsFromField = selectedColumn + ? filteredColumns.filter(col => { + return col.sourceField === selectedColumn.sourceField; + }) + : filteredColumns; + + return ( +
+ + { + setOpen(false); + }} + ownFocus + anchorPosition="rightCenter" + button={ +
+ { + setOpen(!isOpen); + }} + > + + {selectedColumn + ? selectedColumn.label + : i18n.translate('xpack.lens.configureDimension', { + defaultMessage: 'Configure dimension', + })} + + +
+ } + > + + + ({ + label: col.sourceField, + value: col.operationId, + }))} + selectedOptions={ + selectedColumn + ? [ + { + label: selectedColumn.sourceField, + value: selectedColumn.operationId, + }, + ] + : [] + } + singleSelection={{ asPlainText: true }} + isClearable={false} + onChange={choices => { + const column: IndexPatternColumn = columns.find( + ({ operationId }) => operationId === choices[0].value + )!; + const newColumns: IndexPatternPrivateState['columns'] = { + ...props.state.columns, + [props.columnId]: column, + }; + + props.setState({ + ...props.state, + columns: newColumns, + // Order is not meaningful until we aggregate + columnOrder: Object.keys(newColumns), + }); + }} + /> + + +
+ {operations.map(o => ( + col.operationType === o)} + onClick={() => { + if (!selectedColumn) { + return; + } + + const newColumn: IndexPatternColumn = filteredColumns.find( + col => + col.operationType === o && col.sourceField === selectedColumn.sourceField + )!; + + props.setState({ + ...props.state, + columnOrder: _.uniq( + Object.keys(props.state.columns).concat(props.columnId) + ), + columns: { + ...props.state.columns, + [props.columnId]: newColumn, + }, + }); + }} + > + {operationPanels[o].displayName} + + ))} +
+
+
+
+
+
+ ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.scss b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.scss new file mode 100644 index 0000000000000..576f9bb23b5d9 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.scss @@ -0,0 +1,3 @@ +.lns-indexPattern-dimensionPopover { + max-width: 600px; +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index 9a575686a541c..42d85273d5f24 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -12,7 +12,6 @@ import { IndexPatternPersistedState, IndexPatternPrivateState, IndexPatternDataPanel, - IndexPatternDimensionPanel, } from './indexpattern'; import { DatasourcePublicAPI, Operation, Datasource } from '../types'; @@ -226,9 +225,13 @@ describe('IndexPattern Data Source', () => { }, }; const state = await indexPatternDatasource.initialize(queryPersistedState); - expect(indexPatternDatasource.toExpression(state)).toMatchInlineSnapshot( - `"esaggs index=\\"1\\" aggs=\\"{\\"timestamp\\":{\\"date_histogram\\":{\\"field\\":\\"timestamp\\",\\"aggs\\":{\\"document\\":{\\"count\\":{\\"field\\":\\"document\\"}}}}}}\\""` - ); + expect(indexPatternDatasource.toExpression(state)).toMatchInlineSnapshot(` +"esaggs + index=\\"1\\" + metricsAtAllLevels=false + partialRows=false + aggConfigs='{\\"id\\":\\"document\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{\\"field\\":\\"document\\"}},{\\"id\\":\\"timestamp\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"metric\\",\\"params\\":{\\"field\\":\\"timestamp\\"}}'" +`); }); }); @@ -260,88 +263,5 @@ describe('IndexPattern Data Source', () => { } as Operation); }); }); - - describe('renderDimensionPanel', () => { - let state: IndexPatternPrivateState; - - beforeEach(async () => { - state = await indexPatternDatasource.initialize(persistedState); - }); - - it('should render a dimension panel', () => { - const wrapper = shallow( - {}} - columnId={'col2'} - filterOperations={(operation: Operation) => true} - /> - ); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should call the filterOperations function', () => { - const filterOperations = jest.fn().mockReturnValue(true); - - shallow( - {}} - columnId={'col2'} - filterOperations={filterOperations} - /> - ); - - expect(filterOperations).toBeCalled(); - }); - - it('should filter out all selections if the filter returns false', () => { - const wrapper = shallow( - {}} - columnId={'col2'} - filterOperations={() => false} - /> - ); - - expect(wrapper.find(EuiComboBox)!.prop('options')!.length).toEqual(0); - }); - - it('should update the datasource state on selection', () => { - const setState = jest.fn(); - - const wrapper = shallow( - true} - /> - ); - - const comboBox = wrapper.find(EuiComboBox)!; - const firstOption = comboBox.prop('options')![0]; - - comboBox.prop('onChange')!([firstOption]); - - expect(setState).toHaveBeenCalledWith({ - ...state, - columns: { - ...state.columns, - col2: { - operationId: firstOption.value, - label: 'value of timestamp', - dataType: 'date', - isBucketed: false, - operationType: 'value', - sourceField: 'timestamp', - }, - }, - columnOrder: ['col1', 'col2'], - }); - }); - }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 35d26d2482d13..7fee8589c2d1a 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -5,20 +5,20 @@ */ import _ from 'lodash'; -import React, { useState } from 'react'; +import React from 'react'; import { render } from 'react-dom'; import { Chrome } from 'ui/chrome'; import { ToastNotifications } from 'ui/notify/toasts/toast_notifications'; -import { EuiComboBox, EuiPopover, EuiButton, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiComboBox } from '@elastic/eui'; import { Datasource, DataType } from '..'; import { DatasourceDimensionPanelProps, DatasourceDataPanelProps } from '../types'; import { getIndexPatterns } from './loader'; +import { toExpression } from './to_expression'; +import { IndexPatternDimensionPanel } from './dimension_panel'; -type OperationType = 'value' | 'terms' | 'date_histogram' | 'sum' | 'average' | 'count'; +export type OperationType = 'value' | 'terms' | 'date_histogram' | 'sum' | 'average' | 'count'; -const operations: OperationType[] = ['value', 'terms', 'date_histogram', 'sum', 'average', 'count']; - -interface IndexPatternColumn { +export interface IndexPatternColumn { // Public operationId: string; label: string; @@ -97,190 +97,6 @@ export function IndexPatternDataPanel(props: DatasourceDataPanelProps void; -}; - -function getOperationTypesForField({ type }: IndexPatternField): OperationType[] { - switch (type) { - case 'date': - return ['value', 'date_histogram']; - case 'number': - return ['value', 'sum', 'average']; - case 'string': - return ['value', 'terms']; - } - return []; -} - -function getOperationResultType({ type }: IndexPatternField, op: OperationType): DataType { - switch (op) { - case 'value': - return type as DataType; - case 'average': - case 'count': - case 'sum': - return 'number'; - case 'date_histogram': - return 'date'; - case 'terms': - return 'string'; - } -} - -export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) { - const [isOpen, setOpen] = useState(false); - - const fields = props.state.indexPatterns[props.state.currentIndexPatternId].fields; - - const columns: IndexPatternColumn[] = fields - .map((field, index) => { - const validOperations = getOperationTypesForField(field); - - return validOperations.map(op => ({ - operationId: `${index}${op}`, - label: `${op} of ${field.name}`, - dataType: getOperationResultType(field, op), - isBucketed: op !== 'terms' && op !== 'date_histogram', - - operationType: op, - sourceField: field.name, - })); - }) - .reduce((prev, current) => prev.concat(current)); - - columns.push({ - operationId: 'count', - label: 'Count of Documents', - dataType: 'number', - isBucketed: false, - - operationType: 'count', - sourceField: 'documents', - }); - - const filteredColumns = columns.filter(col => { - const { operationId, label, dataType, isBucketed } = col; - - return props.filterOperations({ - id: operationId, - label, - dataType, - isBucketed, - }); - }); - - const selectedColumn: IndexPatternColumn | null = props.state.columns[props.columnId] || null; - - const uniqueColumns = _.uniq(filteredColumns, col => col.operationType); - - const columnsFromFunction = selectedColumn - ? filteredColumns.filter(col => { - return col.operationType === selectedColumn.operationType; - }) - : filteredColumns; - - return ( -
- Dimension Panel - - { - setOpen(false); - }} - ownFocus - anchorPosition="rightCenter" - button={ - { - setOpen(!isOpen); - }} - > - {selectedColumn ? selectedColumn.label : 'Configure dimension'} - - } - > - - -
- {operations.map(o => ( - col.operationType === o)} - onClick={() => { - const newColumn: IndexPatternColumn = uniqueColumns.find( - col => col.operationType === o - )!; - - props.setState({ - ...props.state, - columnOrder: _.uniq( - Object.keys(props.state.columns).concat(props.columnId) - ), - columns: { - ...props.state.columns, - [props.columnId]: newColumn, - }, - }); - }} - > - {o} - - ))} -
-
- - ({ - label: col.sourceField, - value: col.operationId, - }))} - selectedOptions={ - selectedColumn - ? [ - { - label: selectedColumn.sourceField, - value: selectedColumn.operationId, - }, - ] - : [] - } - singleSelection={{ asPlainText: true }} - isClearable={false} - onChange={choices => { - const column: IndexPatternColumn = columns.find( - ({ operationId }) => operationId === choices[0].value - )!; - const newColumns: IndexPatternPrivateState['columns'] = { - ...props.state.columns, - [props.columnId]: column, - }; - - props.setState({ - ...props.state, - columns: newColumns, - // Order is not meaningful until we aggregate - columnOrder: Object.keys(newColumns), - }); - }} - /> - -
-
-
-
- ); -} - export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: ToastNotifications) { // Not stateful. State is persisted to the frame const indexPatternDatasource: Datasource = { @@ -312,47 +128,7 @@ export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: To return { currentIndexPatternId, columns, columnOrder }; }, - toExpression(state: IndexPatternPrivateState) { - if (state.columnOrder.length === 0) { - return ''; - } - - const fieldNames = state.columnOrder.map(col => state.columns[col].sourceField); - const sortedColumns = state.columnOrder.map(col => state.columns[col]); - - if (sortedColumns.every(({ operationType }) => operationType === 'value')) { - return `esdocs index="${state.currentIndexPatternId}" fields="${fieldNames.join( - ', ' - )}" sort="${fieldNames[0]}, DESC"`; - } else if (sortedColumns.length) { - let topAgg: object; - sortedColumns.forEach((col, index) => { - if (topAgg) { - topAgg = { - [fieldNames[index]]: { - [col.operationType]: { - field: col.sourceField, - aggs: topAgg as object, - }, - }, - }; - } else { - topAgg = { - [fieldNames[index]]: { - [col.operationType]: { - field: col.sourceField, - }, - }, - }; - } - }); - - return `esaggs index="${state.currentIndexPatternId}" aggs="${JSON.stringify(topAgg!)}"`; - } - - return ''; - }, - + toExpression, renderDataPanel( domElement: Element, props: DatasourceDataPanelProps diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts new file mode 100644 index 0000000000000..4b4a067c735a6 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -0,0 +1,92 @@ +/* + * 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 _ from 'lodash'; + +import { IndexPatternPrivateState } from './indexpattern'; + +export function toExpression(state: IndexPatternPrivateState) { + if (state.columnOrder.length === 0) { + return ''; + } + + const fieldNames = state.columnOrder.map(col => state.columns[col].sourceField); + const sortedColumns = state.columnOrder.map(col => state.columns[col]); + + if (sortedColumns.every(({ operationType }) => operationType === 'value')) { + return `esdocs index="${state.currentIndexPatternId}" fields="${fieldNames.join(', ')}" sort="${ + fieldNames[0] + }, DESC"`; + } else if (sortedColumns.length) { + const aggs = sortedColumns + .map((col, index) => { + if (col.operationId === 'date_histogram') { + return { + id: fieldNames[index], + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: col.sourceField, + timeRange: { + from: 'now-15m', + to: 'now', + }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }; + } else if (col.operationId === 'terms') { + return { + id: fieldNames[index], + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: col.sourceField, + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }; + } else if (col.operationId === 'count') { + return { + id: fieldNames[index], + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }; + } else { + return { + id: fieldNames[index], + enabled: true, + type: col.operationType, + schema: 'metric', + params: { + field: col.sourceField, + }, + }; + } + }) + .map(agg => JSON.stringify(agg)); + + return `esaggs + index="${state.currentIndexPatternId}" + metricsAtAllLevels=false + partialRows=false + aggConfigs='${aggs.join(',')}'`; + } + + return ''; +} From fe4238a904f317e1bfe6e58a599c03e060684d12 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 4 Jun 2019 12:48:12 -0400 Subject: [PATCH 03/27] Fix dimension labeling and add clear button --- .../dimension_panel.test.tsx.snap | 9 +-- .../dimension_panel.test.tsx | 23 ++++++++ .../indexpattern_plugin/dimension_panel.tsx | 58 +++++++++++++------ .../indexpattern_plugin/indexpattern.tsx | 19 +++--- 4 files changed, 80 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap index f3e1bda3f5d1f..f7a8a92f0969e 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap @@ -1,15 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`IndexPatternDimensionPanel should render a dimension panel 1`] = ` -
+ -
+ } closePopover={[Function]} hasArrow={true} @@ -160,5 +161,5 @@ exports[`IndexPatternDimensionPanel should render a dimension panel 1`] = ` -
+ `; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx index a283449009070..7bd6cb08f1463 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx @@ -289,4 +289,27 @@ Array [ columnOrder: ['col1', 'col2'], }); }); + + it('should clear the dimension with the clear button', () => { + const setState = jest.fn(); + + const wrapper = shallow( + true} + /> + ); + + const clearButton = wrapper.find('[data-test-subj="indexPattern-dimensionPopover-remove"]'); + + clearButton.simulate('click'); + + expect(setState).toHaveBeenCalledWith({ + ...state, + columns: {}, + columnOrder: [], + }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx index dab84bef16690..094c60bda58ae 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx @@ -7,13 +7,21 @@ import _ from 'lodash'; import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiComboBox, EuiPopover, EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiComboBox, + EuiPopover, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; import { DataType, DatasourceDimensionPanelProps } from '../types'; import { IndexPatternColumn, IndexPatternField, IndexPatternPrivateState, OperationType, + columnToOperation, } from './indexpattern'; const operations: OperationType[] = ['value', 'terms', 'date_histogram', 'sum', 'average', 'count']; @@ -55,7 +63,7 @@ const operationPanels: Record< }), ofName: name => i18n.translate('xpack.lens.indexPatternOperations.dateHistogramOf', { - defaultMessage: 'Date Histogram of', + defaultMessage: 'Date Histogram of {name}', values: { name }, }), }, @@ -66,7 +74,7 @@ const operationPanels: Record< }), ofName: name => i18n.translate('xpack.lens.indexPatternOperations.sumOf', { - defaultMessage: 'Sum of', + defaultMessage: 'Sum of {name}', values: { name }, }), }, @@ -77,7 +85,7 @@ const operationPanels: Record< }), ofName: name => i18n.translate('xpack.lens.indexPatternOperations.averageOf', { - defaultMessage: 'Average of', + defaultMessage: 'Average of {name}', values: { name }, }), }, @@ -88,7 +96,7 @@ const operationPanels: Record< }), ofName: name => i18n.translate('xpack.lens.indexPatternOperations.countOf', { - defaultMessage: 'Count of', + defaultMessage: 'Count of {name}', values: { name }, }), }, @@ -166,14 +174,7 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp const columns = getPotentialColumns(props.state); const filteredColumns = columns.filter(col => { - const { operationId, label, dataType, isBucketed } = col; - - return props.filterOperations({ - id: operationId, - label, - dataType, - isBucketed, - }); + return props.filterOperations(columnToOperation(col)); }); const selectedColumn: IndexPatternColumn | null = props.state.columns[props.columnId] || null; @@ -187,7 +188,7 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp : filteredColumns; return ( -
+ + { @@ -214,7 +215,7 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp })} -
+ } > @@ -296,6 +297,29 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp - + {selectedColumn && ( + + { + const newColumns: IndexPatternPrivateState['columns'] = { + ...props.state.columns, + }; + delete newColumns[props.columnId]; + + props.setState({ + ...props.state, + columns: newColumns, + columnOrder: Object.keys(newColumns).filter(key => key !== props.columnId), + }); + }} + /> + + )} + ); } diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 7fee8589c2d1a..ff675a091f977 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -97,6 +97,16 @@ export function IndexPatternDataPanel(props: DatasourceDataPanelProps = { @@ -142,14 +152,7 @@ export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: To return state.columnOrder.map(colId => ({ columnId: colId })); }, getOperationForColumnId: (columnId: string) => { - const column = state.columns[columnId]; - const { dataType, label, isBucketed, operationId } = column; - return { - id: operationId, - label, - dataType, - isBucketed, - }; + return columnToOperation(state.columns[columnId]); }, renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => { From 4d0941681f9376e365e8c65f59d20ead3f110083 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 5 Jun 2019 14:04:43 -0400 Subject: [PATCH 04/27] Support more aggregations, aggregation nesting, rollups, and clearing --- .../indexpattern_plugin/__mocks__/loader.ts | 50 ++++- .../dimension_panel.test.tsx.snap | 32 +++- .../__snapshots__/indexpattern.test.tsx.snap | 6 +- .../dimension_panel.test.tsx | 149 ++++++++++++--- .../indexpattern_plugin/dimension_panel.tsx | 174 +++++------------- .../indexpattern_plugin/indexpattern.scss | 2 +- .../indexpattern_plugin/indexpattern.test.tsx | 40 +++- .../indexpattern_plugin/indexpattern.tsx | 73 +++++++- .../lens/public/indexpattern_plugin/loader.ts | 34 +++- .../indexpattern_plugin/operations.test.ts | 111 +++++++++++ .../public/indexpattern_plugin/operations.ts | 154 ++++++++++++++++ .../indexpattern_plugin/to_expression.ts | 6 +- 12 files changed, 652 insertions(+), 179 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operations.ts diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/loader.ts b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/loader.ts index 1bb56464138d1..95bc8523cd1c3 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/loader.ts @@ -9,7 +9,7 @@ export function getIndexPatterns() { resolve([ { id: '1', - title: 'Fake Index Pattern', + title: 'my-fake-index-pattern', timeFieldName: 'timestamp', fields: [ { @@ -34,7 +34,7 @@ export function getIndexPatterns() { }, { id: '2', - title: 'Fake Rollup Pattern', + title: 'my-fake-rollup-pattern', timeFieldName: 'timestamp', fields: [ { @@ -56,6 +56,52 @@ export function getIndexPatterns() { searchable: true, }, ], + typeMeta: { + params: { + rollup_index: 'my-fake-index-pattern', + }, + aggs: { + terms: { + source: { + agg: 'terms', + }, + }, + date_histogram: { + timestamp: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', + }, + }, + histogram: { + bytes: { + agg: 'histogram', + interval: 1000, + }, + }, + avg: { + bytes: { + agg: 'avg', + }, + }, + max: { + bytes: { + agg: 'max', + }, + }, + min: { + bytes: { + agg: 'min', + }, + }, + sum: { + bytes: { + agg: 'sum', + }, + }, + }, + }, }, ]); }); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap index f7a8a92f0969e..3031aabfc35b5 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap @@ -30,7 +30,7 @@ exports[`IndexPatternDimensionPanel should render a dimension panel 1`] = ` id="col2" isOpen={false} ownFocus={true} - panelClassName="lns-indexPattern-dimensionPopover" + panelClassName="lnsIndexPattern__dimensionPopover" panelPaddingSize="m" > @@ -143,6 +143,32 @@ exports[`IndexPatternDimensionPanel should render a dimension panel 1`] = ` Average + + + Minimum + + + + + Maximum + + { }); describe('getPotentialColumns', () => { - it('should list operations by field', () => { + it('should include priority', () => { + const columns = getPotentialColumns(state, 1); + + expect(columns.every(col => col.suggestedOrder === 1)).toEqual(true); + }); + + it('should list operations by field for a regular index pattern', () => { const columns = getPotentialColumns(state); expect(columns.map(col => [col.sourceField, col.operationType])).toMatchInlineSnapshot(` @@ -110,7 +91,15 @@ Array [ ], Array [ "bytes", - "average", + "avg", + ], + Array [ + "bytes", + "min", + ], + Array [ + "bytes", + "max", ], Array [ "source", @@ -215,7 +204,7 @@ Array [ wrapper.find('[data-test-subj="lns-indexPatternDimension-sum"]').prop('isDisabled') ).toEqual(true); expect( - wrapper.find('[data-test-subj="lns-indexPatternDimension-average"]').prop('isDisabled') + wrapper.find('[data-test-subj="lns-indexPatternDimension-avg"]').prop('isDisabled') ).toEqual(true); expect( wrapper.find('[data-test-subj="lns-indexPatternDimension-count"]').prop('isDisabled') @@ -231,6 +220,7 @@ Array [ setState={setState} columnId={'col2'} filterOperations={() => true} + suggestedPriority={1} /> ); @@ -250,6 +240,7 @@ Array [ isBucketed: false, operationType: 'value', sourceField: 'timestamp', + suggestedOrder: 1, }, }, columnOrder: ['col1', 'col2'], @@ -313,3 +304,101 @@ Array [ }); }); }); + +describe('getColumnOrder', () => { + it('should work for empty columns', () => {}); + + it('should work for one column', () => { + expect( + getColumnOrder({ + col1: { + operationId: 'op1', + label: 'Value of timestamp', + dataType: 'string', + isBucketed: false, + + // Private + operationType: 'value', + sourceField: 'timestamp', + }, + }) + ).toEqual(['col1']); + }); + + it('should put any number of aggregations before metrics', () => { + expect( + getColumnOrder({ + col1: { + operationId: 'op1', + label: 'Top Values of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'value', + sourceField: 'timestamp', + }, + col2: { + operationId: 'op2', + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'value', + sourceField: 'bytes', + }, + col3: { + operationId: 'op3', + label: 'Date Histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + }, + }) + ).toEqual(['col1', 'col3', 'col2']); + }); + + it('should reorder aggregations based on suggested priority', () => { + expect( + getColumnOrder({ + col1: { + operationId: 'op1', + label: 'Top Values of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'value', + sourceField: 'timestamp', + suggestedOrder: 2, + }, + col2: { + operationId: 'op2', + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'value', + sourceField: 'bytes', + suggestedOrder: 0, + }, + col3: { + operationId: 'op3', + label: 'Date Histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + suggestedOrder: 1, + }, + }) + ).toEqual(['col3', 'col1', 'col2']); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx index 094c60bda58ae..e418407a17f63 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx @@ -15,128 +15,29 @@ import { EuiFlexItem, EuiFlexGroup, } from '@elastic/eui'; -import { DataType, DatasourceDimensionPanelProps } from '../types'; -import { - IndexPatternColumn, - IndexPatternField, - IndexPatternPrivateState, - OperationType, - columnToOperation, -} from './indexpattern'; - -const operations: OperationType[] = ['value', 'terms', 'date_histogram', 'sum', 'average', 'count']; +import { DatasourceDimensionPanelProps, DimensionPriority } from '../types'; +import { IndexPatternColumn, IndexPatternPrivateState, columnToOperation } from './indexpattern'; -const operationPanels: Record< - OperationType, - { - type: OperationType; - displayName: string; - ofName: (name: string) => string; - } -> = { - value: { - type: 'value', - displayName: i18n.translate('xpack.lens.indexPatternOperations.value', { - defaultMessage: 'Value', - }), - ofName: name => - i18n.translate('xpack.lens.indexPatternOperations.valueOf', { - defaultMessage: 'Value of {name}', - values: { name }, - }), - }, - terms: { - type: 'terms', - displayName: i18n.translate('xpack.lens.indexPatternOperations.terms', { - defaultMessage: 'Top Values', - }), - ofName: name => - i18n.translate('xpack.lens.indexPatternOperations.termsOf', { - defaultMessage: 'Top Values of {name}', - values: { name }, - }), - }, - date_histogram: { - type: 'date_histogram', - displayName: i18n.translate('xpack.lens.indexPatternOperations.dateHistogram', { - defaultMessage: 'Date Histogram', - }), - ofName: name => - i18n.translate('xpack.lens.indexPatternOperations.dateHistogramOf', { - defaultMessage: 'Date Histogram of {name}', - values: { name }, - }), - }, - sum: { - type: 'sum', - displayName: i18n.translate('xpack.lens.indexPatternOperations.sum', { - defaultMessage: 'Sum', - }), - ofName: name => - i18n.translate('xpack.lens.indexPatternOperations.sumOf', { - defaultMessage: 'Sum of {name}', - values: { name }, - }), - }, - average: { - type: 'average', - displayName: i18n.translate('xpack.lens.indexPatternOperations.average', { - defaultMessage: 'Average', - }), - ofName: name => - i18n.translate('xpack.lens.indexPatternOperations.averageOf', { - defaultMessage: 'Average of {name}', - values: { name }, - }), - }, - count: { - type: 'count', - displayName: i18n.translate('xpack.lens.indexPatternOperations.count', { - defaultMessage: 'Count', - }), - ofName: name => - i18n.translate('xpack.lens.indexPatternOperations.countOf', { - defaultMessage: 'Count of {name}', - values: { name }, - }), - }, -}; +import { + getOperationDisplay, + getOperations, + getOperationTypesForField, + getOperationResultType, +} from './operations'; export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { state: IndexPatternPrivateState; setState: (newState: IndexPatternPrivateState) => void; }; -function getOperationTypesForField({ type }: IndexPatternField): OperationType[] { - switch (type) { - case 'date': - return ['value', 'date_histogram']; - case 'number': - return ['value', 'sum', 'average']; - case 'string': - return ['value', 'terms']; - } - return []; -} - -function getOperationResultType({ type }: IndexPatternField, op: OperationType): DataType { - switch (op) { - case 'value': - return type as DataType; - case 'average': - case 'count': - case 'sum': - return 'number'; - case 'date_histogram': - return 'date'; - case 'terms': - return 'string'; - } -} - -export function getPotentialColumns(state: IndexPatternPrivateState): IndexPatternColumn[] { +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); @@ -149,6 +50,7 @@ export function getPotentialColumns(state: IndexPatternPrivateState): IndexPatte operationType: op, sourceField: field.name, + suggestedOrder, })); }) .reduce((prev, current) => prev.concat(current)); @@ -163,15 +65,36 @@ export function getPotentialColumns(state: IndexPatternPrivateState): IndexPatte operationType: 'count', sourceField: 'documents', + suggestedOrder, }); return columns; } +export function getColumnOrder(columns: Record): string[] { + const entries = Object.entries(columns); + + const [aggregations, metrics] = _.partition(entries, col => col[1].isBucketed); + + return aggregations + .sort(([id, col], [id2, col2]) => { + return ( + // Sort undefined orders last + (col.suggestedOrder !== undefined ? col.suggestedOrder : 3) - + (col2.suggestedOrder !== undefined ? col2.suggestedOrder : 3) + ); + }) + .map(([id]) => id) + .concat(metrics.map(([id]) => id)); +} + export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) { const [isOpen, setOpen] = useState(false); - const columns = getPotentialColumns(props.state); + const operations = getOperations(); + const operationPanels = getOperationDisplay(); + + const columns = getPotentialColumns(props.state, props.suggestedPriority); const filteredColumns = columns.filter(col => { return props.filterOperations(columnToOperation(col)); @@ -192,7 +115,7 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp { setOpen(false); @@ -210,7 +133,7 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp {selectedColumn ? selectedColumn.label - : i18n.translate('xpack.lens.configureDimension', { + : i18n.translate('xpack.lens.indexPattern.configureDimensionLabel', { defaultMessage: 'Configure dimension', })} @@ -251,8 +174,7 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp props.setState({ ...props.state, columns: newColumns, - // Order is not meaningful until we aggregate - columnOrder: Object.keys(newColumns), + columnOrder: getColumnOrder(newColumns), }); }} /> @@ -277,15 +199,15 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp col.operationType === o && col.sourceField === selectedColumn.sourceField )!; + const newColumns = { + ...props.state.columns, + [props.columnId]: newColumn, + }; + props.setState({ ...props.state, - columnOrder: _.uniq( - Object.keys(props.state.columns).concat(props.columnId) - ), - columns: { - ...props.state.columns, - [props.columnId]: newColumn, - }, + columnOrder: getColumnOrder(newColumns), + columns: newColumns, }); }} > @@ -314,7 +236,7 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp props.setState({ ...props.state, columns: newColumns, - columnOrder: Object.keys(newColumns).filter(key => key !== props.columnId), + columnOrder: getColumnOrder(newColumns), }); }} /> diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.scss b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.scss index 576f9bb23b5d9..ac1b7d4ab754b 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.scss +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.scss @@ -1,3 +1,3 @@ -.lns-indexPattern-dimensionPopover { +.lnsIndexPattern__dimensionPopover { max-width: 600px; } diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index 42d85273d5f24..c1466857e01fc 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -20,7 +20,7 @@ jest.mock('./loader'); const expectedIndexPatterns = { 1: { id: '1', - title: 'Fake Index Pattern', + title: 'my-fake-index-pattern', timeFieldName: 'timestamp', fields: [ { @@ -45,7 +45,7 @@ const expectedIndexPatterns = { }, 2: { id: '2', - title: 'Fake Rollup Pattern', + title: 'my-fake-rollup-pattern', timeFieldName: 'timestamp', fields: [ { @@ -53,18 +53,50 @@ const expectedIndexPatterns = { type: 'date', aggregatable: true, searchable: true, + rollupRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', + }, + }, }, { name: 'bytes', type: 'number', aggregatable: true, searchable: true, + rollupRestrictions: { + // Ignored in the UI + histogram: { + agg: 'histogram', + interval: 1000, + }, + avg: { + agg: 'avg', + }, + max: { + agg: 'max', + }, + min: { + agg: 'min', + }, + sum: { + agg: 'sum', + }, + }, }, { name: 'source', type: 'string', aggregatable: true, searchable: true, + rollupRestrictions: { + terms: { + agg: 'terms', + }, + }, }, ], }, @@ -193,7 +225,7 @@ describe('IndexPattern Data Source', () => { }; const state = await indexPatternDatasource.initialize(queryPersistedState); expect(indexPatternDatasource.toExpression(state)).toMatchInlineSnapshot( - `"esdocs index=\\"1\\" fields=\\"source, bytes\\" sort=\\"source, DESC\\""` + `"esdocs index=\\"my-fake-index-pattern\\" fields=\\"source, bytes\\" sort=\\"source, DESC\\""` ); }); @@ -227,7 +259,7 @@ describe('IndexPattern Data Source', () => { const state = await indexPatternDatasource.initialize(queryPersistedState); expect(indexPatternDatasource.toExpression(state)).toMatchInlineSnapshot(` "esaggs - index=\\"1\\" + index=\\"my-fake-index-pattern\\" metricsAtAllLevels=false partialRows=false aggConfigs='{\\"id\\":\\"document\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{\\"field\\":\\"document\\"}},{\\"id\\":\\"timestamp\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"metric\\",\\"params\\":{\\"field\\":\\"timestamp\\"}}'" diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index ff675a091f977..23fddfc6c6419 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -11,12 +11,24 @@ import { Chrome } from 'ui/chrome'; import { ToastNotifications } from 'ui/notify/toasts/toast_notifications'; import { EuiComboBox } from '@elastic/eui'; import { Datasource, DataType } from '..'; -import { DatasourceDimensionPanelProps, DatasourceDataPanelProps } from '../types'; +import { + DatasourceDimensionPanelProps, + DatasourceDataPanelProps, + DimensionPriority, +} from '../types'; import { getIndexPatterns } from './loader'; import { toExpression } from './to_expression'; import { IndexPatternDimensionPanel } from './dimension_panel'; -export type OperationType = 'value' | 'terms' | 'date_histogram' | 'sum' | 'average' | 'count'; +export type OperationType = + | 'value' + | 'terms' + | 'date_histogram' + | 'sum' + | 'avg' + | 'min' + | 'max' + | 'count'; export interface IndexPatternColumn { // Public @@ -28,6 +40,7 @@ export interface IndexPatternColumn { // Private operationType: OperationType; sourceField: string; + suggestedOrder?: DimensionPriority; } export interface IndexPattern { @@ -43,6 +56,19 @@ export interface IndexPatternField { esTypes?: string[]; aggregatable: boolean; searchable: boolean; + rollupRestrictions?: Partial< + Record< + string, + { + agg: string; + interval?: number; + fixed_interval?: string; + calendar_interval?: string; + delay?: string; + time_zone?: string; + } + > + >; } export interface IndexPatternPersistedState { @@ -107,6 +133,41 @@ export function columnToOperation(column: IndexPatternColumn) { }; } +type UnwrapPromise = T extends Promise ? P : T; +type InferFromArray = T extends Array ? P : T; + +function addRollupInfoToFields( + indexPattern: InferFromArray>, void>> +): IndexPattern { + if (!indexPattern.typeMeta || typeof indexPattern.fields === 'string') { + return indexPattern; + } + + const aggs = Object.keys(indexPattern.typeMeta.aggs); + + const newFields = [...(indexPattern.fields as IndexPatternField[])]; + newFields.forEach((field, index) => { + const restrictionsObj: IndexPatternField['rollupRestrictions'] = {}; + aggs.forEach(agg => { + if (indexPattern.typeMeta.aggs[agg] && indexPattern.typeMeta.aggs[agg][field.name]) { + restrictionsObj[agg] = indexPattern.typeMeta.aggs[agg][field.name]; + } + }); + if (Object.keys(restrictionsObj).length) { + newFields[index] = { ...field, rollupRestrictions: restrictionsObj }; + } + }); + + const { id, title, timeFieldName } = indexPattern; + + return { + id, + title, + timeFieldName: timeFieldName || undefined, + fields: newFields, + }; +} + export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: ToastNotifications) { // Not stateful. State is persisted to the frame const indexPatternDatasource: Datasource = { @@ -116,7 +177,7 @@ export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: To if (indexPatternObjects) { indexPatternObjects.forEach(obj => { - indexPatterns[obj.id] = obj; + indexPatterns[obj.id] = addRollupInfoToFields(obj); }); } @@ -157,7 +218,11 @@ export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: To renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => { render( - , + setState(newState)} + {...props} + />, domElement ); }, diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/loader.ts b/x-pack/plugins/lens/public/indexpattern_plugin/loader.ts index 3de7d511c4b49..59e01f36a20ad 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/loader.ts @@ -9,29 +9,55 @@ import { ToastNotifications } from 'ui/notify/toasts/toast_notifications'; import { SavedObjectAttributes } from 'src/legacy/server/saved_objects/service/saved_objects_client'; import { IndexPatternField } from './indexpattern'; -interface IndexPatternAttributes extends SavedObjectAttributes { +interface SavedIndexPatternAttributes extends SavedObjectAttributes { title: string; timeFieldName: string | null; fields: string; fieldFormatMap: string; + typeMeta: string; } +interface SavedRollupObject { + params: { + rollup_index: string; + }; + aggs: Record< + string, + Record< + string, + { + agg: string; + interval?: number; + fixed_interval?: string; + delay?: string; + time_zone?: string; + } + > + >; +} +type SavedRollupInfo = SavedRollupObject | undefined; + export const getIndexPatterns = (chrome: Chrome, toastNotifications: ToastNotifications) => { const savedObjectsClient = chrome.getSavedObjectsClient(); return savedObjectsClient - .find({ + .find({ type: 'index-pattern', perPage: 1000, // TODO: Paginate index patterns }) .then(resp => { return resp.savedObjects.map(savedObject => { - const { id, attributes } = savedObject; + const { id, attributes, type } = savedObject; return Object.assign(attributes, { id, + type, title: attributes.title, fields: (JSON.parse(attributes.fields) as IndexPatternField[]).filter( - ({ type, esTypes }) => type !== 'string' || (esTypes && esTypes.includes('keyword')) + ({ type: fieldType, esTypes }) => + fieldType !== 'string' || (esTypes && esTypes.includes('keyword')) ), + typeMeta: attributes.typeMeta + ? (JSON.parse(attributes.typeMeta) as SavedRollupInfo) + : undefined, }); }); }) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts new file mode 100644 index 0000000000000..742daf425ce2b --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts @@ -0,0 +1,111 @@ +/* + * 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 { getOperationTypesForField } from './operations'; + +describe('getOperationTypesForField', () => { + describe('with aggregatable fields', () => { + it('should return operations on strings', () => { + expect( + getOperationTypesForField({ + type: 'string', + name: 'a', + aggregatable: true, + searchable: true, + }) + ).toEqual(expect.arrayContaining(['value', 'terms'])); + }); + + it('should return operations on numbers', () => { + expect( + getOperationTypesForField({ + type: 'number', + name: 'a', + aggregatable: true, + searchable: true, + }) + ).toEqual(expect.arrayContaining(['value', 'avg', 'sum', 'min', 'max'])); + }); + + it('should return operations on dates', () => { + expect( + getOperationTypesForField({ + type: 'date', + name: 'a', + aggregatable: true, + searchable: true, + }) + ).toEqual(expect.arrayContaining(['value', 'date_histogram'])); + }); + + it('should return no operations on unknown types', () => { + expect( + getOperationTypesForField({ + type: '_source', + name: 'a', + aggregatable: true, + searchable: true, + }) + ).toEqual([]); + }); + }); + + describe('with rollups', () => { + it('should return operations on strings', () => { + expect( + getOperationTypesForField({ + type: 'string', + name: 'a', + aggregatable: true, + searchable: true, + rollupRestrictions: { + terms: { + agg: 'terms', + }, + }, + }) + ).toEqual(expect.arrayContaining(['terms'])); + }); + + it('should return operations on numbers', () => { + expect( + getOperationTypesForField({ + type: 'number', + name: 'a', + aggregatable: true, + searchable: true, + rollupRestrictions: { + min: { + agg: 'min', + }, + max: { + agg: 'max', + }, + }, + }) + ).toEqual(expect.arrayContaining(['min', 'max'])); + }); + + it('should return operations on dates', () => { + expect( + getOperationTypesForField({ + type: 'dates', + name: 'a', + aggregatable: true, + searchable: true, + rollupRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '60m', + delay: '1d', + time_zone: 'UTC', + }, + }, + }) + ).toEqual(expect.arrayContaining(['date_histogram'])); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts new file mode 100644 index 0000000000000..dfb3e6f6a8b74 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts @@ -0,0 +1,154 @@ +/* + * 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 _ from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { DataType } from '../types'; +import { IndexPatternField, OperationType } from './indexpattern'; + +export function getOperations(): OperationType[] { + return ['value', 'terms', 'date_histogram', 'sum', 'avg', 'min', 'max', 'count']; +} + +export function getOperationDisplay(): Record< + OperationType, + { + type: OperationType; + displayName: string; + ofName: (name: string) => string; + } +> { + return { + value: { + type: 'value', + displayName: i18n.translate('xpack.lens.indexPattern.value', { + defaultMessage: 'Value', + }), + ofName: name => + i18n.translate('xpack.lens.indexPattern.valueOf', { + defaultMessage: 'Value of {name}', + values: { name }, + }), + }, + terms: { + type: 'terms', + displayName: i18n.translate('xpack.lens.indexPattern.terms', { + defaultMessage: 'Top Values', + }), + ofName: name => + i18n.translate('xpack.lens.indexPattern.termsOf', { + defaultMessage: 'Top Values of {name}', + values: { name }, + }), + }, + date_histogram: { + type: 'date_histogram', + displayName: i18n.translate('xpack.lens.indexPattern.dateHistogram', { + defaultMessage: 'Date Histogram', + }), + ofName: name => + i18n.translate('xpack.lens.indexPattern.dateHistogramOf', { + defaultMessage: 'Date Histogram of {name}', + values: { name }, + }), + }, + sum: { + type: 'sum', + displayName: i18n.translate('xpack.lens.indexPattern.sum', { + defaultMessage: 'Sum', + }), + ofName: name => + i18n.translate('xpack.lens.indexPattern.sumOf', { + defaultMessage: 'Sum of {name}', + values: { name }, + }), + }, + avg: { + type: 'avg', + displayName: i18n.translate('xpack.lens.indexPattern.average', { + defaultMessage: 'Average', + }), + ofName: name => + i18n.translate('xpack.lens.indexPattern.averageOf', { + defaultMessage: 'Average of {name}', + values: { name }, + }), + }, + min: { + type: 'min', + displayName: i18n.translate('xpack.lens.indexPattern.min', { + defaultMessage: 'Minimum', + }), + ofName: name => + i18n.translate('xpack.lens.indexPattern.minOf', { + defaultMessage: 'Minimum of {name}', + values: { name }, + }), + }, + max: { + type: 'max', + displayName: i18n.translate('xpack.lens.indexPattern.max', { + defaultMessage: 'Maximum', + }), + ofName: name => + i18n.translate('xpack.lens.indexPattern.maxOf', { + defaultMessage: 'Maximum of {name}', + values: { name }, + }), + }, + count: { + type: 'count', + displayName: i18n.translate('xpack.lens.indexPattern.count', { + defaultMessage: 'Count', + }), + ofName: name => + i18n.translate('xpack.lens.indexPattern.countOf', { + defaultMessage: 'Count of {name}', + values: { name }, + }), + }, + }; +} + +export function getOperationTypesForField({ + type, + rollupRestrictions, +}: IndexPatternField): OperationType[] { + if (rollupRestrictions) { + const validOperations = getOperations(); + return Object.keys(rollupRestrictions).filter(key => + // Filter out operations that rollups support, but that aren't yet supported by the client + validOperations.includes(key as OperationType) + ) as OperationType[]; + } + + switch (type) { + case 'date': + return ['value', 'date_histogram']; + case 'number': + return ['value', 'sum', 'avg', 'min', 'max']; + case 'string': + return ['value', 'terms']; + } + return []; +} + +export function getOperationResultType({ type }: IndexPatternField, op: OperationType): DataType { + switch (op) { + case 'value': + return type as DataType; + case 'avg': + case 'min': + case 'max': + case 'count': + case 'sum': + return 'number'; + case 'date_histogram': + return 'date'; + case 'terms': + return 'string'; + } +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts index 4b4a067c735a6..b9c622e074927 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -16,8 +16,10 @@ export function toExpression(state: IndexPatternPrivateState) { const fieldNames = state.columnOrder.map(col => state.columns[col].sourceField); const sortedColumns = state.columnOrder.map(col => state.columns[col]); + const indexName = state.indexPatterns[state.currentIndexPatternId].title; + if (sortedColumns.every(({ operationType }) => operationType === 'value')) { - return `esdocs index="${state.currentIndexPatternId}" fields="${fieldNames.join(', ')}" sort="${ + return `esdocs index="${indexName}" fields="${fieldNames.join(', ')}" sort="${ fieldNames[0] }, DESC"`; } else if (sortedColumns.length) { @@ -82,7 +84,7 @@ export function toExpression(state: IndexPatternPrivateState) { .map(agg => JSON.stringify(agg)); return `esaggs - index="${state.currentIndexPatternId}" + index="${indexName}" metricsAtAllLevels=false partialRows=false aggConfigs='${aggs.join(',')}'`; From 3bf54d4ea7f35a41fe2519a2f37e9ee14b448729 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 5 Jun 2019 17:10:44 -0400 Subject: [PATCH 05/27] Fix esaggs expression --- x-pack/plugins/lens/public/index.ts | 6 +++++ .../indexpattern_plugin/to_expression.ts | 26 +++++++++---------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index f8f074cdb99bf..2cb4d28890603 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -6,6 +6,12 @@ export * from './types'; +import 'ui/autoload/all'; +// Used to run esaggs queries +import 'uiExports/fieldFormats'; +import 'uiExports/search'; +import 'uiExports/visRequestHandlers'; +import 'uiExports/visResponseHandlers'; import { render, unmountComponentAtNode } from 'react-dom'; import { IScope } from 'angular'; import chrome from 'ui/chrome'; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts index b9c622e074927..feaccbb34c56e 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -25,28 +25,28 @@ export function toExpression(state: IndexPatternPrivateState) { } else if (sortedColumns.length) { const aggs = sortedColumns .map((col, index) => { - if (col.operationId === 'date_histogram') { + if (col.operationType === 'date_histogram') { return { - id: fieldNames[index], + id: state.columnOrder[index], enabled: true, type: 'date_histogram', schema: 'segment', params: { field: col.sourceField, timeRange: { - from: 'now-15m', + from: 'now-1d', to: 'now', }, useNormalizedEsInterval: true, - interval: 'auto', + interval: '1h', drop_partials: false, min_doc_count: 1, extended_bounds: {}, }, }; - } else if (col.operationId === 'terms') { + } else if (col.operationType === 'terms') { return { - id: fieldNames[index], + id: state.columnOrder[index], enabled: true, type: 'terms', schema: 'segment', @@ -61,9 +61,9 @@ export function toExpression(state: IndexPatternPrivateState) { missingBucketLabel: 'Missing', }, }; - } else if (col.operationId === 'count') { + } else if (col.operationType === 'count') { return { - id: fieldNames[index], + id: state.columnOrder[index], enabled: true, type: 'count', schema: 'metric', @@ -71,7 +71,7 @@ export function toExpression(state: IndexPatternPrivateState) { }; } else { return { - id: fieldNames[index], + id: state.columnOrder[index], enabled: true, type: col.operationType, schema: 'metric', @@ -84,10 +84,10 @@ export function toExpression(state: IndexPatternPrivateState) { .map(agg => JSON.stringify(agg)); return `esaggs - index="${indexName}" - metricsAtAllLevels=false - partialRows=false - aggConfigs='${aggs.join(',')}'`; + index="${state.currentIndexPatternId}" + metricsAtAllLevels="false" + partialRows="false" + aggConfigs='[${aggs.join(',')}]'`; } return ''; From 8c505983873767332234267a9943a2893f50c392 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 6 Jun 2019 16:47:18 -0400 Subject: [PATCH 06/27] Increase top-level test coverage of dimension panel --- .../__mocks__/operations.ts | 19 ++ .../dimension_panel.test.tsx.snap | 12 +- .../dimension_panel.test.tsx | 238 ++++++------------ .../indexpattern_plugin/dimension_panel.tsx | 64 +---- .../indexpattern_plugin/indexpattern.test.tsx | 8 +- .../indexpattern_plugin/indexpattern.tsx | 1 + .../indexpattern_plugin/operations.test.ts | 210 +++++++++++++++- .../public/indexpattern_plugin/operations.ts | 71 +++++- .../indexpattern_plugin/to_expression.ts | 120 ++++----- 9 files changed, 441 insertions(+), 302 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts new file mode 100644 index 0000000000000..0d7fcdecbc340 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +const actual = jest.requireActual('../operations'); + +jest.spyOn(actual, 'getPotentialColumns'); +jest.spyOn(actual, 'getColumnOrder'); + +export const { + getPotentialColumns, + getColumnOrder, + getOperations, + getOperationDisplay, + getOperationTypesForField, + getOperationResultType, +} = actual; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap index 3031aabfc35b5..770c5409eab6b 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap @@ -47,21 +47,21 @@ exports[`IndexPatternDimensionPanel should render a dimension panel 1`] = ` onChange={[Function]} options={ Array [ - Object { - "label": "timestamp", - "value": "0value", - }, Object { "label": "bytes", "value": "1value", }, + Object { + "label": "documents", + "value": "count", + }, Object { "label": "source", "value": "2value", }, Object { - "label": "documents", - "value": "count", + "label": "timestamp", + "value": "0value", }, ] } diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx index 9e8247e74c02e..23eb4f4359803 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx @@ -8,7 +8,10 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { EuiComboBox } from '@elastic/eui'; import { IndexPatternPrivateState } from './indexpattern'; -import { getColumnOrder, getPotentialColumns, IndexPatternDimensionPanel } from './dimension_panel'; +import { getColumnOrder, getPotentialColumns } from './operations'; +import { IndexPatternDimensionPanel } from './dimension_panel'; + +jest.mock('./operations'); const expectedIndexPatterns = { 1: { @@ -50,7 +53,7 @@ describe('IndexPatternDimensionPanel', () => { col1: { operationId: 'op1', label: 'Value of timestamp', - dataType: 'string', + dataType: 'date', isBucketed: false, // Private @@ -59,63 +62,8 @@ describe('IndexPatternDimensionPanel', () => { }, }, }; - }); - - describe('getPotentialColumns', () => { - it('should include priority', () => { - const columns = getPotentialColumns(state, 1); - expect(columns.every(col => col.suggestedOrder === 1)).toEqual(true); - }); - - it('should list operations by field for a regular index pattern', () => { - const columns = getPotentialColumns(state); - - expect(columns.map(col => [col.sourceField, col.operationType])).toMatchInlineSnapshot(` -Array [ - Array [ - "timestamp", - "value", - ], - Array [ - "timestamp", - "date_histogram", - ], - Array [ - "bytes", - "value", - ], - Array [ - "bytes", - "sum", - ], - Array [ - "bytes", - "avg", - ], - Array [ - "bytes", - "min", - ], - Array [ - "bytes", - "max", - ], - Array [ - "source", - "value", - ], - Array [ - "source", - "terms", - ], - Array [ - "documents", - "count", - ], -] -`); - }); + jest.clearAllMocks(); }); it('should render a dimension panel', () => { @@ -148,6 +96,20 @@ Array [ ).toEqual('Configure dimension'); }); + it('should pass the right arguments to getPotentialColumns', async () => { + shallow( + {}} + columnId={'col1'} + filterOperations={() => true} + suggestedPriority={1} + /> + ); + + expect(getPotentialColumns as jest.Mock).toHaveBeenCalledWith(state, 1); + }); + it('should call the filterOperations function', () => { const filterOperations = jest.fn().mockReturnValue(true); @@ -176,7 +138,27 @@ Array [ expect(wrapper.find(EuiComboBox)!.prop('options')!.length).toEqual(0); }); - it("should disable functions that won't work with dates", () => { + it('should list all field names in sorted order', () => { + const wrapper = shallow( + {}} + columnId={'col1'} + filterOperations={() => true} + /> + ); + + const options = wrapper.find(EuiComboBox).prop('options'); + + expect(options!.map(({ label }) => label)).toEqual([ + 'bytes', + 'documents', + 'source', + 'timestamp', + ]); + }); + + it("should disable functions that won't work with the current column", () => { const setState = jest.fn(); const wrapper = shallow( @@ -233,20 +215,38 @@ Array [ ...state, columns: { ...state.columns, - col2: { - operationId: firstOption.value, - label: 'Value of timestamp', - dataType: 'date', - isBucketed: false, - operationType: 'value', - sourceField: 'timestamp', - suggestedOrder: 1, - }, + col2: expect.objectContaining({ + sourceField: firstOption.label, + // Other parts of this don't matter for this test + }), }, columnOrder: ['col1', 'col2'], }); }); + it('should always request the new sort order when changing the function', () => { + const setState = jest.fn(); + + const wrapper = shallow( + true} + suggestedPriority={1} + /> + ); + + wrapper.find('[data-test-subj="lns-indexPatternDimension-date_histogram"]').simulate('click'); + + expect(getColumnOrder).toHaveBeenCalledWith({ + col1: expect.objectContaining({ + sourceField: 'timestamp', + operationType: 'date_histogram', + }), + }); + }); + it('should update the datasource state when the user makes a selection', () => { const setState = jest.fn(); @@ -268,14 +268,14 @@ Array [ ...state, columns: { ...state.columns, - col2: { + col2: expect.objectContaining({ operationId: firstField.value, label: 'Value of bytes', dataType: 'number', isBucketed: false, operationType: 'value', sourceField: 'bytes', - }, + }), }, columnOrder: ['col1', 'col2'], }); @@ -304,101 +304,3 @@ Array [ }); }); }); - -describe('getColumnOrder', () => { - it('should work for empty columns', () => {}); - - it('should work for one column', () => { - expect( - getColumnOrder({ - col1: { - operationId: 'op1', - label: 'Value of timestamp', - dataType: 'string', - isBucketed: false, - - // Private - operationType: 'value', - sourceField: 'timestamp', - }, - }) - ).toEqual(['col1']); - }); - - it('should put any number of aggregations before metrics', () => { - expect( - getColumnOrder({ - col1: { - operationId: 'op1', - label: 'Top Values of category', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'value', - sourceField: 'timestamp', - }, - col2: { - operationId: 'op2', - label: 'Average of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'value', - sourceField: 'bytes', - }, - col3: { - operationId: 'op3', - label: 'Date Histogram of timestamp', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - sourceField: 'timestamp', - }, - }) - ).toEqual(['col1', 'col3', 'col2']); - }); - - it('should reorder aggregations based on suggested priority', () => { - expect( - getColumnOrder({ - col1: { - operationId: 'op1', - label: 'Top Values of category', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'value', - sourceField: 'timestamp', - suggestedOrder: 2, - }, - col2: { - operationId: 'op2', - label: 'Average of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'value', - sourceField: 'bytes', - suggestedOrder: 0, - }, - col3: { - operationId: 'op3', - label: 'Date Histogram of timestamp', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - sourceField: 'timestamp', - suggestedOrder: 1, - }, - }) - ).toEqual(['col3', 'col1', 'col2']); - }); -}); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx index e418407a17f63..fd3600a3a616a 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx @@ -15,14 +15,14 @@ import { EuiFlexItem, EuiFlexGroup, } from '@elastic/eui'; -import { DatasourceDimensionPanelProps, DimensionPriority } from '../types'; +import { DatasourceDimensionPanelProps } from '../types'; import { IndexPatternColumn, IndexPatternPrivateState, columnToOperation } from './indexpattern'; import { getOperationDisplay, getOperations, - getOperationTypesForField, - getOperationResultType, + getPotentialColumns, + getColumnOrder, } from './operations'; export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { @@ -30,64 +30,6 @@ export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { setState: (newState: IndexPatternPrivateState) => void; }; -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, - })); - }) - .reduce((prev, current) => prev.concat(current)); - - columns.push({ - operationId: 'count', - label: i18n.translate('xpack.lens.indexPatternOperations.countOfDocuments', { - defaultMessage: 'Count of Documents', - }), - dataType: 'number', - isBucketed: false, - - operationType: 'count', - sourceField: 'documents', - suggestedOrder, - }); - - return columns; -} - -export function getColumnOrder(columns: Record): string[] { - const entries = Object.entries(columns); - - const [aggregations, metrics] = _.partition(entries, col => col[1].isBucketed); - - return aggregations - .sort(([id, col], [id2, col2]) => { - return ( - // Sort undefined orders last - (col.suggestedOrder !== undefined ? col.suggestedOrder : 3) - - (col2.suggestedOrder !== undefined ? col2.suggestedOrder : 3) - ); - }) - .map(([id]) => id) - .concat(metrics.map(([id]) => id)); -} - export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) { const [isOpen, setOpen] = useState(false); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index 8bd2a0421eacf..48f9c74c481de 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -259,10 +259,10 @@ describe('IndexPattern Data Source', () => { const state = await indexPatternDatasource.initialize(queryPersistedState); expect(indexPatternDatasource.toExpression(state)).toMatchInlineSnapshot(` "esaggs - index=\\"my-fake-index-pattern\\" - metricsAtAllLevels=false - partialRows=false - aggConfigs='{\\"id\\":\\"document\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{\\"field\\":\\"document\\"}},{\\"id\\":\\"timestamp\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"metric\\",\\"params\\":{\\"field\\":\\"timestamp\\"}}'" + index=\\"1\\" + metricsAtAllLevels=\\"false\\" + partialRows=\\"false\\" + aggConfigs='[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"timeRange\\":{\\"from\\":\\"now-1d\\",\\"to\\":\\"now\\"},\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1h\\",\\"drop_partials\\":false,\\"min_doc_count\\":1,\\"extended_bounds\\":{}}}]'" `); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index a69beacad7207..2528b9ad606dd 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -201,6 +201,7 @@ export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: To }, toExpression, + renderDataPanel( domElement: Element, props: DatasourceDataPanelProps diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts index 742daf425ce2b..4155e48ed5f8b 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts @@ -4,7 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOperationTypesForField } from './operations'; +import { getOperationTypesForField, getPotentialColumns, getColumnOrder } from './operations'; +import { IndexPatternPrivateState } from './indexpattern'; + +const expectedIndexPatterns = { + 1: { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + fields: [ + { + name: 'timestamp', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + ], + }, +}; describe('getOperationTypesForField', () => { describe('with aggregatable fields', () => { @@ -108,4 +137,183 @@ describe('getOperationTypesForField', () => { ).toEqual(expect.arrayContaining(['date_histogram'])); }); }); + + describe('getPotentialColumns', () => { + let state: IndexPatternPrivateState; + + beforeEach(() => { + state = { + indexPatterns: expectedIndexPatterns, + currentIndexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'Value of timestamp', + dataType: 'date', + isBucketed: false, + + // Private + operationType: 'value', + sourceField: 'timestamp', + }, + }, + }; + }); + + it('should include priority', () => { + const columns = getPotentialColumns(state, 1); + + expect(columns.every(col => col.suggestedOrder === 1)).toEqual(true); + }); + + it('should list operations by field for a regular index pattern', () => { + const columns = getPotentialColumns(state); + + expect(columns.map(col => [col.sourceField, col.operationType])).toMatchInlineSnapshot(` +Array [ + Array [ + "bytes", + "value", + ], + Array [ + "bytes", + "sum", + ], + Array [ + "bytes", + "avg", + ], + Array [ + "bytes", + "min", + ], + Array [ + "bytes", + "max", + ], + Array [ + "documents", + "count", + ], + Array [ + "source", + "value", + ], + Array [ + "source", + "terms", + ], + Array [ + "timestamp", + "value", + ], + Array [ + "timestamp", + "date_histogram", + ], +] +`); + }); + }); +}); + +describe('getColumnOrder', () => { + it('should work for empty columns', () => { + expect(getColumnOrder({})).toEqual([]); + }); + + it('should work for one column', () => { + expect( + getColumnOrder({ + col1: { + operationId: 'op1', + label: 'Value of timestamp', + dataType: 'string', + isBucketed: false, + + // Private + operationType: 'value', + sourceField: 'timestamp', + }, + }) + ).toEqual(['col1']); + }); + + it('should put any number of aggregations before metrics', () => { + expect( + getColumnOrder({ + col1: { + operationId: 'op1', + label: 'Top Values of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'value', + sourceField: 'timestamp', + }, + col2: { + operationId: 'op2', + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'value', + sourceField: 'bytes', + }, + col3: { + operationId: 'op3', + label: 'Date Histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + }, + }) + ).toEqual(['col1', 'col3', 'col2']); + }); + + it('should reorder aggregations based on suggested priority', () => { + expect( + getColumnOrder({ + col1: { + operationId: 'op1', + label: 'Top Values of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'value', + sourceField: 'timestamp', + suggestedOrder: 2, + }, + col2: { + operationId: 'op2', + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'value', + sourceField: 'bytes', + suggestedOrder: 0, + }, + col3: { + operationId: 'op3', + label: 'Date Histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + suggestedOrder: 1, + }, + }) + ).toEqual(['col3', 'col1', 'col2']); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts index dfb3e6f6a8b74..ad0d086b304eb 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts @@ -6,8 +6,13 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { DataType } from '../types'; -import { IndexPatternField, OperationType } from './indexpattern'; +import { DataType, DimensionPriority } from '../types'; +import { + IndexPatternColumn, + IndexPatternField, + IndexPatternPrivateState, + OperationType, +} from './indexpattern'; export function getOperations(): OperationType[] { return ['value', 'terms', 'date_histogram', 'sum', 'avg', 'min', 'max', 'count']; @@ -152,3 +157,65 @@ export function getOperationResultType({ type }: IndexPatternField, op: Operatio return 'string'; } } + +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, + })); + }) + .reduce((prev, current) => prev.concat(current)); + + columns.push({ + operationId: 'count', + label: i18n.translate('xpack.lens.indexPatternOperations.countOfDocuments', { + defaultMessage: 'Count of Documents', + }), + dataType: 'number', + isBucketed: false, + + operationType: 'count', + sourceField: 'documents', + suggestedOrder, + }); + + columns.sort(({ sourceField }, { sourceField: sourceField2 }) => + sourceField.localeCompare(sourceField2) + ); + + return columns; +} + +export function getColumnOrder(columns: Record): string[] { + const entries = Object.entries(columns); + + const [aggregations, metrics] = _.partition(entries, col => col[1].isBucketed); + + return aggregations + .sort(([id, col], [id2, col2]) => { + return ( + // Sort undefined orders last + (col.suggestedOrder !== undefined ? col.suggestedOrder : Number.MAX_SAFE_INTEGER) - + (col2.suggestedOrder !== undefined ? col2.suggestedOrder : Number.MAX_SAFE_INTEGER) + ); + }) + .map(([id]) => id) + .concat(metrics.map(([id]) => id)); +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts index feaccbb34c56e..9e9f113665fdb 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -10,7 +10,7 @@ import { IndexPatternPrivateState } from './indexpattern'; export function toExpression(state: IndexPatternPrivateState) { if (state.columnOrder.length === 0) { - return ''; + return null; } const fieldNames = state.columnOrder.map(col => state.columns[col].sourceField); @@ -23,71 +23,71 @@ export function toExpression(state: IndexPatternPrivateState) { fieldNames[0] }, DESC"`; } else if (sortedColumns.length) { - const aggs = sortedColumns - .map((col, index) => { - if (col.operationType === 'date_histogram') { - return { - id: state.columnOrder[index], - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: { - field: col.sourceField, - timeRange: { - from: 'now-1d', - to: 'now', - }, - useNormalizedEsInterval: true, - interval: '1h', - drop_partials: false, - min_doc_count: 1, - extended_bounds: {}, + const firstMetric = sortedColumns.findIndex(({ isBucketed }) => !isBucketed); + const aggs = sortedColumns.map((col, index) => { + if (col.operationType === 'date_histogram') { + return { + id: state.columnOrder[index], + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: col.sourceField, + // TODO: This range should be passed in from somewhere else + timeRange: { + from: 'now-1d', + to: 'now', }, - }; - } else if (col.operationType === 'terms') { - return { - id: state.columnOrder[index], - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: col.sourceField, - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }; - } else if (col.operationType === 'count') { - return { - id: state.columnOrder[index], - enabled: true, - type: 'count', - schema: 'metric', - params: {}, - }; - } else { - return { - id: state.columnOrder[index], - enabled: true, - type: col.operationType, - schema: 'metric', - params: { - field: col.sourceField, - }, - }; - } - }) - .map(agg => JSON.stringify(agg)); + useNormalizedEsInterval: true, + interval: '1h', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }; + } else if (col.operationType === 'terms') { + return { + id: state.columnOrder[index], + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: col.sourceField, + orderBy: state.columnOrder[firstMetric] || undefined, + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }; + } else if (col.operationType === 'count') { + return { + id: state.columnOrder[index], + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }; + } else { + return { + id: state.columnOrder[index], + enabled: true, + type: col.operationType, + schema: 'metric', + params: { + field: col.sourceField, + }, + }; + } + }); return `esaggs index="${state.currentIndexPatternId}" metricsAtAllLevels="false" partialRows="false" - aggConfigs='[${aggs.join(',')}]'`; + aggConfigs='${JSON.stringify(aggs)}'`; } return ''; From 8359b280beb5dc98cc51ef46da252faead4fcdf1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 7 Jun 2019 15:31:03 +0200 Subject: [PATCH 07/27] start work on param editors for operations --- .../indexpattern_plugin/dimension_panel.tsx | 112 ++++++++++------- .../indexpattern_plugin/indexpattern.tsx | 49 +++++++- .../public/indexpattern_plugin/operations.ts | 114 +++++++++++++++--- .../public/indexpattern_plugin/params.tsx | 83 +++++++++++++ .../indexpattern_plugin/to_expression.ts | 6 +- 5 files changed, 299 insertions(+), 65 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/params.tsx diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx index fd3600a3a616a..d8336ef2e712f 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx @@ -16,7 +16,13 @@ import { EuiFlexGroup, } from '@elastic/eui'; import { DatasourceDimensionPanelProps } from '../types'; -import { IndexPatternColumn, IndexPatternPrivateState, columnToOperation } from './indexpattern'; +import { + IndexPatternColumn, + IndexPatternPrivateState, + columnToOperation, + FieldBasedIndexPatternColumn, +} from './indexpattern'; +import { getParamEditors } from './params'; import { getOperationDisplay, @@ -44,14 +50,24 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp const selectedColumn: IndexPatternColumn | null = props.state.columns[props.columnId] || null; - const uniqueColumnsByField = _.uniq(filteredColumns, col => col.sourceField); + const fieldColumns = filteredColumns.filter( + col => 'sourceField' in col + ) as FieldBasedIndexPatternColumn[]; + + const uniqueColumnsByField = _.uniq(fieldColumns, col => col.sourceField); const functionsFromField = selectedColumn ? filteredColumns.filter(col => { - return col.sourceField === selectedColumn.sourceField; + return ( + !('sourceField' in selectedColumn) || + !('sourceField' in col) || + col.sourceField === selectedColumn.sourceField + ); }) : filteredColumns; + const ParamEditor = selectedColumn && getParamEditors()[selectedColumn.operationType]; + return ( @@ -85,42 +101,53 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp > - ({ - label: col.sourceField, - value: col.operationId, - }))} - selectedOptions={ - selectedColumn - ? [ - { - label: selectedColumn.sourceField, - value: selectedColumn.operationId, - }, - ] - : [] - } - singleSelection={{ asPlainText: true }} - isClearable={false} - onChange={choices => { - const column: IndexPatternColumn = columns.find( - ({ operationId }) => operationId === choices[0].value - )!; - const newColumns: IndexPatternPrivateState['columns'] = { - ...props.state.columns, - [props.columnId]: column, - }; - - props.setState({ - ...props.state, - columns: newColumns, - columnOrder: getColumnOrder(newColumns), - }); - }} - /> + {(!selectedColumn || 'sourceField' in selectedColumn) && ( + ({ + label: col.sourceField, + value: col.operationId, + }))} + selectedOptions={ + selectedColumn && 'sourceField' in selectedColumn + ? [ + { + label: selectedColumn.sourceField, + value: selectedColumn.operationId, + }, + ] + : [] + } + singleSelection={{ asPlainText: true }} + isClearable={false} + onChange={choices => { + const column: IndexPatternColumn = columns.find( + ({ operationId }) => operationId === choices[0].value + )!; + const newColumns: IndexPatternPrivateState['columns'] = { + ...props.state.columns, + [props.columnId]: column, + }; + + props.setState({ + ...props.state, + columns: newColumns, + columnOrder: getColumnOrder(newColumns), + }); + }} + /> + )} + {ParamEditor && ( + + + + )}
{operations.map(o => ( @@ -132,13 +159,12 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp } isDisabled={!functionsFromField.some(col => col.operationType === o)} onClick={() => { - if (!selectedColumn) { - return; - } - const newColumn: IndexPatternColumn = filteredColumns.find( col => - col.operationType === o && col.sourceField === selectedColumn.sourceField + col.operationType === o && + (!('sourceField' in col) || + !('sourceField' in selectedColumn) || + col.sourceField === selectedColumn.sourceField) )!; const newColumns = { diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 2528b9ad606dd..0b69066e98f18 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -31,7 +31,18 @@ export type OperationType = | 'max' | 'count'; -export interface IndexPatternColumn { +export type IndexPatternColumn = + | DateHistogramIndexPatternColumn + | TermsIndexPatternColumn + | ValueIndexPatternColumn + | SumIndexPatternColumn + | AvgIndexPatternColumn + | MinIndexPatternColumn + | MaxIndexPatternColumn + | SumIndexPatternColumn + | CountIndexPatternColumn; + +export interface BaseIndexPatternColumn { // Public operationId: string; label: string; @@ -40,10 +51,44 @@ export interface IndexPatternColumn { // Private operationType: OperationType; - sourceField: string; suggestedOrder?: DimensionPriority; } +type Omit = Pick>; +type ParameterlessIndexPatternColumn< + TOperationType extends OperationType, + TBase extends BaseIndexPatternColumn = FieldBasedIndexPatternColumn +> = Omit & { operationType: TOperationType }; + +export interface FieldBasedIndexPatternColumn extends BaseIndexPatternColumn { + sourceField: string; +} + +export interface DateHistogramIndexPatternColumn extends FieldBasedIndexPatternColumn { + operationType: 'date_histogram'; + params: { + interval: string; + }; +} + +export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { + operationType: 'terms'; + params: { + size: number; + orderBy: { type: 'alphabetical' } | { type: 'column'; columnId: string }; + }; +} + +export type ValueIndexPatternColumn = ParameterlessIndexPatternColumn<'value'>; +export type CountIndexPatternColumn = ParameterlessIndexPatternColumn< + 'count', + BaseIndexPatternColumn +>; +export type SumIndexPatternColumn = ParameterlessIndexPatternColumn<'sum'>; +export type AvgIndexPatternColumn = ParameterlessIndexPatternColumn<'avg'>; +export type MinIndexPatternColumn = ParameterlessIndexPatternColumn<'min'>; +export type MaxIndexPatternColumn = ParameterlessIndexPatternColumn<'max'>; + export interface IndexPattern { id: string; fields: IndexPatternField[]; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts index ad0d086b304eb..938e858e24e4a 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts @@ -141,10 +141,10 @@ export function getOperationTypesForField({ return []; } -export function getOperationResultType({ type }: IndexPatternField, op: OperationType): DataType { +export function getOperationResultType(op: OperationType, field?: IndexPatternField): DataType { switch (op) { case 'value': - return type as DataType; + return field!.type as DataType; case 'avg': case 'min': case 'max': @@ -158,28 +158,104 @@ export function getOperationResultType({ type }: IndexPatternField, op: Operatio } } +function buildColumnForOperationType( + op: OperationType, + operationId: string, + suggestedOrder?: DimensionPriority, + field?: IndexPatternField +): IndexPatternColumn { + const operationPanels = getOperationDisplay(); + const baseColumn = { + operationId, + label: operationPanels[op].ofName(field ? field.name : ''), + dataType: getOperationResultType(op, field), + operationType: op, + suggestedOrder, + }; + + const fieldColumn = { + sourceField: field ? field.name : '', + }; + + switch (op) { + case 'avg': + return { + ...baseColumn, + ...fieldColumn, + isBucketed: false, + operationType: op, + }; + case 'min': + return { + ...baseColumn, + ...fieldColumn, + isBucketed: false, + operationType: op, + }; + case 'max': + return { + ...baseColumn, + ...fieldColumn, + isBucketed: false, + operationType: op, + }; + case 'sum': + return { + ...baseColumn, + ...fieldColumn, + isBucketed: false, + operationType: op, + }; + case 'count': + return { + ...baseColumn, + isBucketed: false, + operationType: op, + }; + case 'value': + return { + ...baseColumn, + ...fieldColumn, + isBucketed: false, + operationType: op, + }; + case 'date_histogram': + return { + ...baseColumn, + ...fieldColumn, + isBucketed: true, + operationType: op, + params: { + interval: '1h', + }, + }; + case 'terms': + return { + ...baseColumn, + ...fieldColumn, + isBucketed: true, + operationType: op, + params: { + size: 5, + orderBy: { type: 'alphabetical' }, + }, + }; + } +} + 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 => + buildColumnForOperationType(op, `${op}${index}`, suggestedOrder, field) + ); }) .reduce((prev, current) => prev.concat(current)); @@ -192,13 +268,15 @@ export function getPotentialColumns( isBucketed: false, operationType: 'count', - sourceField: 'documents', suggestedOrder, }); - columns.sort(({ sourceField }, { sourceField: sourceField2 }) => - sourceField.localeCompare(sourceField2) - ); + columns.sort((column1, column2) => { + if ('sourceField' in column1 && 'sourceField' in column2) { + return column1.sourceField.localeCompare(column2.sourceField); + } + return column1.operationType.localeCompare(column2.operationType); + }); return columns; } diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/params.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/params.tsx new file mode 100644 index 0000000000000..5bc2b58dd4134 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/params.tsx @@ -0,0 +1,83 @@ +/* + * 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 _ from 'lodash'; +import React from 'react'; +import { EuiForm, EuiFormRow, EuiRange } from '@elastic/eui'; +import { + IndexPatternPrivateState, + OperationType, + DateHistogramIndexPatternColumn, +} from './indexpattern'; + +export interface ParamEditorProps { + state: IndexPatternPrivateState; + setState: (newState: IndexPatternPrivateState) => void; + columnId: string; +} + +type PropType = C extends React.ComponentType ? P : unknown; + +// Add ticks to EuiRange component props +const FixedEuiRange = (EuiRange as unknown) as React.ComponentType< + PropType & { + ticks: Array<{ + label: string; + value: number; + }>; + } +>; + +const paramEditors: Partial>> = { + date_histogram: ({ state, setState, columnId }) => { + const column = state.columns[columnId] as DateHistogramIndexPatternColumn; + const intervals = ['M', 'w', 'd', 'h']; + + function intervalToNumeric(interval: string) { + return intervals.indexOf(interval); + } + + function numericToInterval(i: number) { + return intervals[i]; + } + return ( + + + ({ label: interval, value: index }))} + onChange={(e: React.ChangeEvent) => + setState({ + ...state, + columns: { + ...state.columns, + [columnId]: { + ...column, + params: { + ...column.params, + interval: numericToInterval(Number(e.target.value)), + }, + }, + }, + }) + } + aria-label="Level of Detail" + /> + + + ); + }, +}; + +export function getParamEditors(): Partial< + Record> +> { + return paramEditors; +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts index 9e9f113665fdb..ef155858e48d1 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -13,8 +13,10 @@ export function toExpression(state: IndexPatternPrivateState) { return null; } - const fieldNames = state.columnOrder.map(col => state.columns[col].sourceField); const sortedColumns = state.columnOrder.map(col => state.columns[col]); + const fieldNames = sortedColumns.map(column => + 'sourceField' in column ? column.sourceField : undefined + ); const indexName = state.indexPatterns[state.currentIndexPatternId].title; @@ -39,7 +41,7 @@ export function toExpression(state: IndexPatternPrivateState) { to: 'now', }, useNormalizedEsInterval: true, - interval: '1h', + interval: col.params.interval, drop_partials: false, min_doc_count: 1, extended_bounds: {}, From 6a3e6f89e3035f6c35cfd69ad3d330974134e878 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 7 Jun 2019 17:13:19 +0200 Subject: [PATCH 08/27] add params for terms --- .../public/indexpattern_plugin/operations.ts | 5 ++ .../public/indexpattern_plugin/params.tsx | 90 ++++++++++++++++++- .../indexpattern_plugin/to_expression.ts | 5 +- 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts index 938e858e24e4a..01d44ab42b137 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts @@ -18,6 +18,11 @@ export function getOperations(): OperationType[] { return ['value', 'terms', 'date_histogram', 'sum', 'avg', 'min', 'max', 'count']; } +export function isMetricOperation(operationType: OperationType) { + const metricOpTypes: OperationType[] = ['sum', 'avg', 'min', 'max', 'count']; + return metricOpTypes.includes(operationType); +} + export function getOperationDisplay(): Record< OperationType, { diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/params.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/params.tsx index 5bc2b58dd4134..9f4ba87bb43ff 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/params.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/params.tsx @@ -6,12 +6,14 @@ import _ from 'lodash'; import React from 'react'; -import { EuiForm, EuiFormRow, EuiRange } from '@elastic/eui'; +import { EuiForm, EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; import { IndexPatternPrivateState, OperationType, DateHistogramIndexPatternColumn, + TermsIndexPatternColumn, } from './indexpattern'; +import { isMetricOperation } from './operations'; export interface ParamEditorProps { state: IndexPatternPrivateState; @@ -24,7 +26,7 @@ type PropType = C extends React.ComponentType ? P : unknown; // Add ticks to EuiRange component props const FixedEuiRange = (EuiRange as unknown) as React.ComponentType< PropType & { - ticks: Array<{ + ticks?: Array<{ label: string; value: number; }>; @@ -74,6 +76,90 @@ const paramEditors: Partial ); }, + terms: ({ state, setState, columnId: currentColumnId }) => { + const currentColumn = state.columns[currentColumnId] as TermsIndexPatternColumn; + function toValue(orderBy: TermsIndexPatternColumn['params']['orderBy']) { + if (orderBy.type === 'alphabetical') { + return orderBy.type; + } + return `${orderBy.type}-${orderBy.columnId}`; + } + + function fromValue(value: string): TermsIndexPatternColumn['params']['orderBy'] { + if (value === 'alphabetical') { + return { type: 'alphabetical' }; + } + const parts = value.split('-'); + return { + type: 'column', + columnId: parts[1], + }; + } + + const orderOptions = Object.entries(state.columns) + .filter(([_columnId, column]) => isMetricOperation(column.operationType)) + .map(([columnId, column]) => { + return { + value: toValue({ type: 'column', columnId }), + text: column.label, + }; + }); + orderOptions.push({ + value: toValue({ type: 'alphabetical' }), + text: 'Alphabetical', + }); + + return ( + + + ) => + setState({ + ...state, + columns: { + ...state.columns, + [currentColumnId]: { + ...currentColumn, + params: { + ...currentColumn.params, + size: Number(e.target.value), + }, + }, + }, + }) + } + aria-label="Number of values" + /> + + + ) => + setState({ + ...state, + columns: { + ...state.columns, + [currentColumnId]: { + ...currentColumn, + params: { + ...currentColumn.params, + orderBy: fromValue(e.target.value), + }, + }, + }, + }) + } + /> + + + ); + }, }; export function getParamEditors(): Partial< diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts index ef155858e48d1..3a26f31c03cea 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -55,9 +55,10 @@ export function toExpression(state: IndexPatternPrivateState) { schema: 'segment', params: { field: col.sourceField, - orderBy: state.columnOrder[firstMetric] || undefined, + orderBy: + col.params.orderBy.type === 'alphabetical' ? '_key' : col.params.orderBy.columnId, order: 'desc', - size: 5, + size: col.params.size, otherBucket: false, otherBucketLabel: 'Other', missingBucket: false, From 9cb80a85add81f5af0386fc7fcb7ab6bb0df3d31 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 7 Jun 2019 17:14:28 +0200 Subject: [PATCH 09/27] remove unused variable --- x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts index 3a26f31c03cea..5dafa90a5031e 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -25,7 +25,6 @@ export function toExpression(state: IndexPatternPrivateState) { fieldNames[0] }, DESC"`; } else if (sortedColumns.length) { - const firstMetric = sortedColumns.findIndex(({ isBucketed }) => !isBucketed); const aggs = sortedColumns.map((col, index) => { if (col.operationType === 'date_histogram') { return { From 5510db066d8437405f82e7a0081064e13d4a23d4 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 11 Jun 2019 15:51:25 +0200 Subject: [PATCH 10/27] refactor operations --- .../indexpattern_plugin/dimension_panel.tsx | 5 +- .../indexpattern_plugin/indexpattern.tsx | 10 +- .../public/indexpattern_plugin/operations.ts | 304 ------------------ .../indexpattern_plugin/operations/count.tsx | 33 ++ .../operations/date_histogram.tsx | 104 ++++++ .../indexpattern_plugin/operations/index.ts | 149 +++++++++ .../operations/metrics.tsx | 101 ++++++ .../{params.tsx => operations/terms.tsx} | 102 +++--- .../indexpattern_plugin/operations/value.tsx | 46 +++ 9 files changed, 477 insertions(+), 377 deletions(-) delete mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operations.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operations/count.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx rename x-pack/plugins/lens/public/indexpattern_plugin/{params.tsx => operations/terms.tsx} (60%) create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operations/value.tsx diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx index d8336ef2e712f..c8b2ce817b7ae 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx @@ -22,13 +22,13 @@ import { columnToOperation, FieldBasedIndexPatternColumn, } from './indexpattern'; -import { getParamEditors } from './params'; import { getOperationDisplay, getOperations, getPotentialColumns, getColumnOrder, + operationDefinitionMap, } from './operations'; export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { @@ -66,7 +66,8 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp }) : filteredColumns; - const ParamEditor = selectedColumn && getParamEditors()[selectedColumn.operationType]; + const ParamEditor = + selectedColumn && operationDefinitionMap[selectedColumn.operationType].paramEditor; return ( diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index a58498307383f..ae4cf2ac7ebe2 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -21,15 +21,7 @@ import { getIndexPatterns } from './loader'; import { toExpression } from './to_expression'; import { IndexPatternDimensionPanel } from './dimension_panel'; -export type OperationType = - | 'value' - | 'terms' - | 'date_histogram' - | 'sum' - | 'avg' - | 'min' - | 'max' - | 'count'; +export type OperationType = IndexPatternColumn['operationType']; export type IndexPatternColumn = | DateHistogramIndexPatternColumn diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts deleted file mode 100644 index 8d32f65cf3ec6..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts +++ /dev/null @@ -1,304 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { DataType, DimensionPriority } from '../types'; -import { - IndexPatternColumn, - IndexPatternField, - IndexPatternPrivateState, - OperationType, -} from './indexpattern'; - -export function getOperations(): OperationType[] { - return ['value', 'terms', 'date_histogram', 'sum', 'avg', 'min', 'max', 'count']; -} - -export function isMetricOperation(operationType: OperationType) { - const metricOpTypes: OperationType[] = ['sum', 'avg', 'min', 'max', 'count']; - return metricOpTypes.includes(operationType); -} - -export function getOperationDisplay(): Record< - OperationType, - { - type: OperationType; - displayName: string; - ofName: (name: string) => string; - } -> { - return { - value: { - type: 'value', - displayName: i18n.translate('xpack.lens.indexPattern.value', { - defaultMessage: 'Value', - }), - ofName: name => - i18n.translate('xpack.lens.indexPattern.valueOf', { - defaultMessage: 'Value of {name}', - values: { name }, - }), - }, - terms: { - type: 'terms', - displayName: i18n.translate('xpack.lens.indexPattern.terms', { - defaultMessage: 'Top Values', - }), - ofName: name => - i18n.translate('xpack.lens.indexPattern.termsOf', { - defaultMessage: 'Top Values of {name}', - values: { name }, - }), - }, - date_histogram: { - type: 'date_histogram', - displayName: i18n.translate('xpack.lens.indexPattern.dateHistogram', { - defaultMessage: 'Date Histogram', - }), - ofName: name => - i18n.translate('xpack.lens.indexPattern.dateHistogramOf', { - defaultMessage: 'Date Histogram of {name}', - values: { name }, - }), - }, - sum: { - type: 'sum', - displayName: i18n.translate('xpack.lens.indexPattern.sum', { - defaultMessage: 'Sum', - }), - ofName: name => - i18n.translate('xpack.lens.indexPattern.sumOf', { - defaultMessage: 'Sum of {name}', - values: { name }, - }), - }, - avg: { - type: 'avg', - displayName: i18n.translate('xpack.lens.indexPattern.average', { - defaultMessage: 'Average', - }), - ofName: name => - i18n.translate('xpack.lens.indexPattern.averageOf', { - defaultMessage: 'Average of {name}', - values: { name }, - }), - }, - min: { - type: 'min', - displayName: i18n.translate('xpack.lens.indexPattern.min', { - defaultMessage: 'Minimum', - }), - ofName: name => - i18n.translate('xpack.lens.indexPattern.minOf', { - defaultMessage: 'Minimum of {name}', - values: { name }, - }), - }, - max: { - type: 'max', - displayName: i18n.translate('xpack.lens.indexPattern.max', { - defaultMessage: 'Maximum', - }), - ofName: name => - i18n.translate('xpack.lens.indexPattern.maxOf', { - defaultMessage: 'Maximum of {name}', - values: { name }, - }), - }, - count: { - type: 'count', - displayName: i18n.translate('xpack.lens.indexPattern.count', { - defaultMessage: 'Count', - }), - ofName: name => - i18n.translate('xpack.lens.indexPattern.countOf', { - defaultMessage: 'Count of {name}', - values: { name }, - }), - }, - }; -} - -export function getOperationTypesForField({ - type, - aggregationRestrictions, -}: IndexPatternField): OperationType[] { - if (aggregationRestrictions) { - const validOperations = getOperations(); - return Object.keys(aggregationRestrictions).filter(key => - // Filter out operations that are available, but that aren't yet supported by the client - validOperations.includes(key as OperationType) - ) as OperationType[]; - } - - switch (type) { - case 'date': - return ['value', 'date_histogram']; - case 'number': - return ['value', 'sum', 'avg', 'min', 'max']; - case 'string': - return ['value', 'terms']; - } - return []; -} - -export function getOperationResultType(op: OperationType, field?: IndexPatternField): DataType { - switch (op) { - case 'value': - return field!.type as DataType; - case 'avg': - case 'min': - case 'max': - case 'count': - case 'sum': - return 'number'; - case 'date_histogram': - return 'date'; - case 'terms': - return 'string'; - } -} - -function buildColumnForOperationType( - op: OperationType, - operationId: string, - suggestedOrder?: DimensionPriority, - field?: IndexPatternField -): IndexPatternColumn { - const operationPanels = getOperationDisplay(); - const baseColumn = { - operationId, - label: operationPanels[op].ofName(field ? field.name : ''), - dataType: getOperationResultType(op, field), - operationType: op, - suggestedOrder, - }; - - const fieldColumn = { - sourceField: field ? field.name : '', - }; - - switch (op) { - case 'avg': - return { - ...baseColumn, - ...fieldColumn, - isBucketed: false, - operationType: op, - }; - case 'min': - return { - ...baseColumn, - ...fieldColumn, - isBucketed: false, - operationType: op, - }; - case 'max': - return { - ...baseColumn, - ...fieldColumn, - isBucketed: false, - operationType: op, - }; - case 'sum': - return { - ...baseColumn, - ...fieldColumn, - isBucketed: false, - operationType: op, - }; - case 'count': - return { - ...baseColumn, - isBucketed: false, - operationType: op, - }; - case 'value': - return { - ...baseColumn, - ...fieldColumn, - isBucketed: false, - operationType: op, - }; - case 'date_histogram': - return { - ...baseColumn, - ...fieldColumn, - isBucketed: true, - operationType: op, - params: { - interval: '1h', - }, - }; - case 'terms': - return { - ...baseColumn, - ...fieldColumn, - isBucketed: true, - operationType: op, - params: { - size: 5, - orderBy: { type: 'alphabetical' }, - }, - }; - } -} - -export function getPotentialColumns( - state: IndexPatternPrivateState, - suggestedOrder?: DimensionPriority -): IndexPatternColumn[] { - const fields = state.indexPatterns[state.currentIndexPatternId].fields; - - const columns: IndexPatternColumn[] = fields - .map((field, index) => { - const validOperations = getOperationTypesForField(field); - - return validOperations.map(op => - buildColumnForOperationType(op, `${op}${index}`, suggestedOrder, field) - ); - }) - .reduce((prev, current) => prev.concat(current)); - - columns.push({ - operationId: 'count', - label: i18n.translate('xpack.lens.indexPatternOperations.countOfDocuments', { - defaultMessage: 'Count of Documents', - }), - dataType: 'number', - isBucketed: false, - - operationType: 'count', - suggestedOrder, - }); - - columns.sort((column1, column2) => { - if ('sourceField' in column1 && 'sourceField' in column2) { - return column1.sourceField.localeCompare(column2.sourceField); - } - return column1.operationType.localeCompare(column2.operationType); - }); - - return columns; -} - -export function getColumnOrder(columns: Record): string[] { - const entries = Object.entries(columns); - - const [aggregations, metrics] = _.partition(entries, col => col[1].isBucketed); - - return aggregations - .sort(([id, col], [id2, col2]) => { - return ( - // Sort undefined orders last - (col.suggestedOrder !== undefined ? col.suggestedOrder : Number.MAX_SAFE_INTEGER) - - (col2.suggestedOrder !== undefined ? col2.suggestedOrder : Number.MAX_SAFE_INTEGER) - ); - }) - .map(([id]) => id) - .concat(metrics.map(([id]) => id)); -} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/count.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/count.tsx new file mode 100644 index 0000000000000..2746190fd84a2 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/count.tsx @@ -0,0 +1,33 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { CountIndexPatternColumn } from '../indexpattern'; +import { DimensionPriority } from '../../types'; +import { OperationDefinition } from '.'; + +export const countOperation: OperationDefinition = { + type: 'count', + displayName: i18n.translate('xpack.lens.indexPattern.count', { + defaultMessage: 'Count', + }), + isApplicableWithoutField: true, + isApplicableForField: ({ aggregationRestrictions, type }) => { + return false; + }, + buildColumn(operationId: string, suggestedOrder?: DimensionPriority): CountIndexPatternColumn { + return { + operationId, + label: i18n.translate('xpack.lens.indexPattern.countOf', { + defaultMessage: 'Count of documents', + }), + dataType: 'number', + operationType: 'count', + suggestedOrder, + isBucketed: false, + }; + }, +}; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx new file mode 100644 index 0000000000000..866578aa6cde9 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx @@ -0,0 +1,104 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiForm, EuiFormRow, EuiRange } from '@elastic/eui'; +import { IndexPatternField, DateHistogramIndexPatternColumn } from '../indexpattern'; +import { DimensionPriority } from '../../types'; +import { OperationDefinition } from '.'; + +type PropType = C extends React.ComponentType ? P : unknown; + +// Add ticks to EuiRange component props +const FixedEuiRange = (EuiRange as unknown) as React.ComponentType< + PropType & { + ticks?: Array<{ + label: string; + value: number; + }>; + } +>; + +function ofName(name: string) { + return i18n.translate('xpack.lens.indexPattern.dateHistogramOf', { + defaultMessage: 'Date Histogram of {name}', + values: { name }, + }); +} + +export const dateHistogramOperation: OperationDefinition = { + type: 'date_histogram', + displayName: i18n.translate('xpack.lens.indexPattern.dateHistogram', { + defaultMessage: 'Date Histogram', + }), + isApplicableWithoutField: false, + isApplicableForField: ({ aggregationRestrictions, type }) => { + return Boolean( + type === 'date' && (!aggregationRestrictions || aggregationRestrictions.date_histogram) + ); + }, + buildColumn( + operationId: string, + suggestedOrder?: DimensionPriority, + field?: IndexPatternField + ): DateHistogramIndexPatternColumn { + return { + operationId, + label: ofName(field ? field.name : ''), + dataType: 'date', + operationType: 'date_histogram', + suggestedOrder, + sourceField: field ? field.name : '', + isBucketed: true, + params: { + interval: 'h', + }, + }; + }, + paramEditor: ({ state, setState, columnId }) => { + const column = state.columns[columnId] as DateHistogramIndexPatternColumn; + const intervals = ['M', 'w', 'd', 'h']; + + function intervalToNumeric(interval: string) { + return intervals.indexOf(interval); + } + + function numericToInterval(i: number) { + return intervals[i]; + } + return ( + + + ({ label: interval, value: index }))} + onChange={(e: React.ChangeEvent) => + setState({ + ...state, + columns: { + ...state.columns, + [columnId]: { + ...column, + params: { + ...column.params, + interval: numericToInterval(Number(e.target.value)), + }, + }, + }, + }) + } + aria-label="Level of Detail" + /> + + + ); + }, +}; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts new file mode 100644 index 0000000000000..486efd3320af0 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts @@ -0,0 +1,149 @@ +/* + * 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 _ from 'lodash'; +import { DimensionPriority } from '../../types'; +import { + IndexPatternColumn, + IndexPatternField, + IndexPatternPrivateState, + OperationType, + BaseIndexPatternColumn, +} from '../indexpattern'; +import { termsOperation } from './terms'; +import { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; +import { dateHistogramOperation } from './date_histogram'; +import { valueOperation } from './value'; +import { countOperation } from './count'; + +export function getOperations(): OperationType[] { + return ['value', 'terms', 'date_histogram', 'sum', 'avg', 'min', 'max', 'count']; +} +type PossibleOperationDefinitions< + U extends IndexPatternColumn = IndexPatternColumn +> = U extends IndexPatternColumn ? OperationDefinition : never; + +type PartialOperationDefinitionMap< + U extends PossibleOperationDefinitions = PossibleOperationDefinitions +> = U extends PossibleOperationDefinitions ? { [K in U['type']]: U } : never; + +type UnionToIntersection = (U extends U ? (k: U) => void : never) extends ((k: infer I) => void) + ? I + : never; + +export type AllOperationDefinitions = UnionToIntersection; + +export const operationDefinitionMap: AllOperationDefinitions = { + terms: termsOperation, + date_histogram: dateHistogramOperation, + min: minOperation, + max: maxOperation, + avg: averageOperation, + sum: sumOperation, + value: valueOperation, + count: countOperation, +}; +const operationDefinitions: PossibleOperationDefinitions[] = Object.values(operationDefinitionMap); + +export interface ParamEditorProps { + state: IndexPatternPrivateState; + setState: (newState: IndexPatternPrivateState) => void; + columnId: string; +} +export interface OperationDefinition { + type: C['operationType']; + displayName: string; + // TODO make this a function dependend on the indexpattern with typeMeta information + isApplicableWithoutField: boolean; + isApplicableForField: (field: IndexPatternField) => boolean; + buildColumn: ( + operationId: string, + suggestedOrder?: DimensionPriority, + field?: IndexPatternField + ) => C; + paramEditor?: React.ComponentType; +} + +export function getOperationDisplay() { + const display = {} as Record< + OperationType, + { + type: OperationType; + displayName: string; + } + >; + operationDefinitions.forEach(({ type, displayName }) => { + display[type] = { + type, + displayName, + }; + }); + return display; +} + +export function getOperationTypesForField(field: IndexPatternField): OperationType[] { + return operationDefinitions + .filter(definition => definition.isApplicableForField(field)) + .map(({ type }) => type); +} + +function buildColumnForOperationType( + op: T, + operationId: string, + suggestedOrder?: DimensionPriority, + field?: IndexPatternField +): IndexPatternColumn { + return operationDefinitionMap[op].buildColumn(operationId, suggestedOrder, field); +} + +export function getPotentialColumns( + state: IndexPatternPrivateState, + suggestedOrder?: DimensionPriority +): IndexPatternColumn[] { + const fields = state.indexPatterns[state.currentIndexPatternId].fields; + + const columns: IndexPatternColumn[] = fields + .map((field, index) => { + const validOperations = getOperationTypesForField(field); + + return validOperations.map(op => + buildColumnForOperationType(op, `${op}${index}`, suggestedOrder, field) + ); + }) + .reduce((prev, current) => prev.concat(current)); + + operationDefinitions.forEach(operation => { + if (operation.isApplicableWithoutField) { + columns.push(operation.buildColumn(operation.type, suggestedOrder)); + } + }); + + columns.sort((column1, column2) => { + if ('sourceField' in column1 && 'sourceField' in column2) { + return column1.sourceField.localeCompare(column2.sourceField); + } + return column1.operationType.localeCompare(column2.operationType); + }); + + return columns; +} + +export function getColumnOrder(columns: Record): string[] { + const entries = Object.entries(columns); + + const [aggregations, metrics] = _.partition(entries, col => col[1].isBucketed); + + return aggregations + .sort(([id, col], [id2, col2]) => { + return ( + // Sort undefined orders last + (col.suggestedOrder !== undefined ? col.suggestedOrder : Number.MAX_SAFE_INTEGER) - + (col2.suggestedOrder !== undefined ? col2.suggestedOrder : Number.MAX_SAFE_INTEGER) + ); + }) + .map(([id]) => id) + .concat(metrics.map(([id]) => id)); +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx new file mode 100644 index 0000000000000..eb309b105716a --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx @@ -0,0 +1,101 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + IndexPatternField, + FieldBasedIndexPatternColumn, + MinIndexPatternColumn, + SumIndexPatternColumn, + AvgIndexPatternColumn, + MaxIndexPatternColumn, +} from '../indexpattern'; +import { DimensionPriority } from '../../types'; +import { OperationDefinition } from '.'; + +function buildMetricOperation( + type: T['operationType'], + displayName: string, + ofName: (name: string) => string +) { + const operationDefinition: OperationDefinition = { + type, + displayName, + isApplicableWithoutField: false, + isApplicableForField: ({ aggregationRestrictions, type: fieldType }: IndexPatternField) => { + return Boolean( + fieldType === 'number' && (!aggregationRestrictions || aggregationRestrictions[fieldType]) + ); + }, + buildColumn( + operationId: string, + suggestedOrder?: DimensionPriority, + field?: IndexPatternField + ): T { + if (!field) { + throw new Error(`Invariant: A ${type} operation can only be built with a field`); + } + return { + operationId, + label: ofName(field ? field.name : ''), + dataType: 'number', + operationType: type, + suggestedOrder, + sourceField: field ? field.name : '', + isBucketed: false, + } as T; + }, + }; + return operationDefinition; +} + +export const minOperation = buildMetricOperation( + 'min', + i18n.translate('xpack.lens.indexPattern.min', { + defaultMessage: 'Minimum', + }), + name => + i18n.translate('xpack.lens.indexPattern.minOf', { + defaultMessage: 'Minimum of {name}', + values: { name }, + }) +); + +export const maxOperation = buildMetricOperation( + 'max', + i18n.translate('xpack.lens.indexPattern.min', { + defaultMessage: 'Maximum', + }), + name => + i18n.translate('xpack.lens.indexPattern.minOf', { + defaultMessage: 'Maximum of {name}', + values: { name }, + }) +); + +export const averageOperation = buildMetricOperation( + 'avg', + i18n.translate('xpack.lens.indexPattern.min', { + defaultMessage: 'Average', + }), + name => + i18n.translate('xpack.lens.indexPattern.minOf', { + defaultMessage: 'Average of {name}', + values: { name }, + }) +); + +export const sumOperation = buildMetricOperation( + 'sum', + i18n.translate('xpack.lens.indexPattern.min', { + defaultMessage: 'Sum', + }), + name => + i18n.translate('xpack.lens.indexPattern.minOf', { + defaultMessage: 'Sum of {name}', + values: { name }, + }) +); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/params.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx similarity index 60% rename from x-pack/plugins/lens/public/indexpattern_plugin/params.tsx rename to x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx index 9f4ba87bb43ff..3dbc4979fe84f 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/params.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx @@ -4,22 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiForm, EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; -import { - IndexPatternPrivateState, - OperationType, - DateHistogramIndexPatternColumn, - TermsIndexPatternColumn, -} from './indexpattern'; -import { isMetricOperation } from './operations'; - -export interface ParamEditorProps { - state: IndexPatternPrivateState; - setState: (newState: IndexPatternPrivateState) => void; - columnId: string; -} +import { IndexPatternField, TermsIndexPatternColumn } from '../indexpattern'; +import { DimensionPriority } from '../../types'; +import { OperationDefinition } from '.'; type PropType = C extends React.ComponentType ? P : unknown; @@ -33,50 +23,44 @@ const FixedEuiRange = (EuiRange as unknown) as React.ComponentType< } >; -const paramEditors: Partial>> = { - date_histogram: ({ state, setState, columnId }) => { - const column = state.columns[columnId] as DateHistogramIndexPatternColumn; - const intervals = ['M', 'w', 'd', 'h']; - - function intervalToNumeric(interval: string) { - return intervals.indexOf(interval); - } +function ofName(name: string) { + return i18n.translate('xpack.lens.indexPattern.termsOf', { + defaultMessage: 'Top Values of {name}', + values: { name }, + }); +} - function numericToInterval(i: number) { - return intervals[i]; - } - return ( - - - ({ label: interval, value: index }))} - onChange={(e: React.ChangeEvent) => - setState({ - ...state, - columns: { - ...state.columns, - [columnId]: { - ...column, - params: { - ...column.params, - interval: numericToInterval(Number(e.target.value)), - }, - }, - }, - }) - } - aria-label="Level of Detail" - /> - - +export const termsOperation: OperationDefinition = { + type: 'terms', + displayName: i18n.translate('xpack.lens.indexPattern.terms', { + defaultMessage: 'Top Values', + }), + isApplicableWithoutField: false, + isApplicableForField: ({ aggregationRestrictions, type }) => { + return Boolean( + type === 'string' && (!aggregationRestrictions || aggregationRestrictions.terms) ); }, - terms: ({ state, setState, columnId: currentColumnId }) => { + buildColumn( + operationId: string, + suggestedOrder?: DimensionPriority, + field?: IndexPatternField + ): TermsIndexPatternColumn { + return { + operationId, + label: ofName(field ? field.name : ''), + dataType: 'string', + operationType: 'terms', + suggestedOrder, + sourceField: field ? field.name : '', + isBucketed: true, + params: { + size: 5, + orderBy: { type: 'alphabetical' }, + }, + }; + }, + paramEditor: ({ state, setState, columnId: currentColumnId }) => { const currentColumn = state.columns[currentColumnId] as TermsIndexPatternColumn; function toValue(orderBy: TermsIndexPatternColumn['params']['orderBy']) { if (orderBy.type === 'alphabetical') { @@ -97,7 +81,7 @@ const paramEditors: Partial isMetricOperation(column.operationType)) + .filter(([_columnId, column]) => !column.isBucketed) .map(([columnId, column]) => { return { value: toValue({ type: 'column', columnId }), @@ -161,9 +145,3 @@ const paramEditors: Partial> -> { - return paramEditors; -} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/value.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/value.tsx new file mode 100644 index 0000000000000..d91783ca28de8 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/value.tsx @@ -0,0 +1,46 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { IndexPatternField, ValueIndexPatternColumn } from '../indexpattern'; +import { DataType, DimensionPriority } from '../../types'; +import { OperationDefinition } from '.'; + +function ofName(name: string) { + return i18n.translate('xpack.lens.indexPattern.valueOf', { + defaultMessage: 'Value of {name}', + values: { name }, + }); +} + +export const valueOperation: OperationDefinition = { + type: 'value', + displayName: i18n.translate('xpack.lens.indexPattern.value', { + defaultMessage: 'Value', + }), + isApplicableWithoutField: false, + isApplicableForField: ({ aggregationRestrictions, type }) => { + return !aggregationRestrictions; + }, + buildColumn( + operationId: string, + suggestedOrder?: DimensionPriority, + field?: IndexPatternField + ): ValueIndexPatternColumn { + if (!field) { + throw new Error('Invariant: value operation is only valid on a field'); + } + return { + operationId, + label: ofName(field ? field.name : ''), + dataType: field.type as DataType, + operationType: 'value', + suggestedOrder, + sourceField: field.name, + isBucketed: false, + }; + }, +}; From 404944f856a4b0420ab7566ac51c1d18e02a7315 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 11 Jun 2019 17:07:20 +0200 Subject: [PATCH 11/27] move esaggs configurations to individual operation files --- .../indexpattern_plugin/operations/count.tsx | 7 ++ .../operations/date_histogram.tsx | 19 +++++ .../indexpattern_plugin/operations/index.ts | 1 + .../operations/metrics.tsx | 9 +++ .../indexpattern_plugin/operations/terms.tsx | 17 ++++ .../indexpattern_plugin/operations/value.tsx | 3 + .../indexpattern_plugin/to_expression.ts | 77 ++++--------------- 7 files changed, 72 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/count.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/count.tsx index 2746190fd84a2..b9040450f3897 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/count.tsx @@ -30,4 +30,11 @@ export const countOperation: OperationDefinition = { isBucketed: false, }; }, + toEsAggsConfig: (column, columnId) => ({ + id: columnId, + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }), }; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx index 866578aa6cde9..68849545d01a4 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx @@ -59,6 +59,25 @@ export const dateHistogramOperation: OperationDefinition ({ + id: columnId, + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: column.sourceField, + // TODO: This range should be passed in from somewhere else + timeRange: { + from: 'now-1d', + to: 'now', + }, + useNormalizedEsInterval: true, + interval: column.params.interval, + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }), paramEditor: ({ state, setState, columnId }) => { const column = state.columns[columnId] as DateHistogramIndexPatternColumn; const intervals = ['M', 'w', 'd', 'h']; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts index 486efd3320af0..7ed064402b6d1 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts @@ -65,6 +65,7 @@ export interface OperationDefinition C; paramEditor?: React.ComponentType; + toEsAggsConfig: (column: C, columnId: string) => unknown; } export function getOperationDisplay() { diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx index eb309b105716a..3e4a834416f59 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx @@ -48,6 +48,15 @@ function buildMetricOperation( isBucketed: false, } as T; }, + toEsAggsConfig: (column, columnId) => ({ + id: columnId, + enabled: true, + type: column.operationType, + schema: 'metric', + params: { + field: column.sourceField, + }, + }), }; return operationDefinition; } diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx index 3dbc4979fe84f..13071a54d2d4b 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx @@ -60,6 +60,23 @@ export const termsOperation: OperationDefinition = { }, }; }, + toEsAggsConfig: (column, columnId) => ({ + id: columnId, + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: column.sourceField, + orderBy: + column.params.orderBy.type === 'alphabetical' ? '_key' : column.params.orderBy.columnId, + order: 'desc', + size: column.params.size, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }), paramEditor: ({ state, setState, columnId: currentColumnId }) => { const currentColumn = state.columns[currentColumnId] as TermsIndexPatternColumn; function toValue(orderBy: TermsIndexPatternColumn['params']['orderBy']) { diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/value.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/value.tsx index d91783ca28de8..399284980abf3 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/value.tsx @@ -43,4 +43,7 @@ export const valueOperation: OperationDefinition = { isBucketed: false, }; }, + toEsAggsConfig() { + throw new Error('Invariant: The value operation does not have an esaggs config'); + }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts index 5dafa90a5031e..8d351ed5d60b2 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -6,7 +6,8 @@ import _ from 'lodash'; -import { IndexPatternPrivateState } from './indexpattern'; +import { IndexPatternPrivateState, IndexPatternColumn } from './indexpattern'; +import { operationDefinitionMap, OperationDefinition } from './operations'; export function toExpression(state: IndexPatternPrivateState) { if (state.columnOrder.length === 0) { @@ -14,75 +15,29 @@ export function toExpression(state: IndexPatternPrivateState) { } const sortedColumns = state.columnOrder.map(col => state.columns[col]); - const fieldNames = sortedColumns.map(column => - 'sourceField' in column ? column.sourceField : undefined - ); const indexName = state.indexPatterns[state.currentIndexPatternId].title; + function getEsAggsConfig(column: C, columnId: string) { + // Typescript is not smart enough to infer that definitionMap[C['operationType']] is always OperationDefinition, + // but this is made sure by the typing in operations/index.ts + const operationDefinition = (operationDefinitionMap[ + column.operationType + ] as unknown) as OperationDefinition; + return operationDefinition.toEsAggsConfig(column, columnId); + } + if (sortedColumns.every(({ operationType }) => operationType === 'value')) { + const fieldNames = sortedColumns.map(column => + 'sourceField' in column ? column.sourceField : undefined + ); + return `esdocs index="${indexName}" fields="${fieldNames.join(', ')}" sort="${ fieldNames[0] }, DESC"`; } else if (sortedColumns.length) { const aggs = sortedColumns.map((col, index) => { - if (col.operationType === 'date_histogram') { - return { - id: state.columnOrder[index], - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: { - field: col.sourceField, - // TODO: This range should be passed in from somewhere else - timeRange: { - from: 'now-1d', - to: 'now', - }, - useNormalizedEsInterval: true, - interval: col.params.interval, - drop_partials: false, - min_doc_count: 1, - extended_bounds: {}, - }, - }; - } else if (col.operationType === 'terms') { - return { - id: state.columnOrder[index], - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: col.sourceField, - orderBy: - col.params.orderBy.type === 'alphabetical' ? '_key' : col.params.orderBy.columnId, - order: 'desc', - size: col.params.size, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }; - } else if (col.operationType === 'count') { - return { - id: state.columnOrder[index], - enabled: true, - type: 'count', - schema: 'metric', - params: {}, - }; - } else { - return { - id: state.columnOrder[index], - enabled: true, - type: col.operationType, - schema: 'metric', - params: { - field: col.sourceField, - }, - }; - } + return getEsAggsConfig(col, state.columnOrder[index]); }); return `esaggs From 8e0dc8686bb5fcccc14f1cc86ae88ffddb8e019f Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 12 Jun 2019 14:17:41 +0200 Subject: [PATCH 12/27] implement new edit flow --- .../editor_frame/frame_layout.tsx | 6 +- .../indexpattern_plugin/dimension_panel.tsx | 323 ++++++++++-------- .../indexpattern_plugin/indexpattern.tsx | 1 - .../operations/date_histogram.tsx | 2 +- .../indexpattern_plugin/operations/index.ts | 7 +- .../indexpattern_plugin/operations/terms.tsx | 53 +-- 6 files changed, 220 insertions(+), 172 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx index f62722bf71b85..640a0571513d0 100644 --- a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx +++ b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx @@ -18,9 +18,9 @@ export function FrameLayout(props: FrameLayoutProps) { return ( {/* TODO style this and add workspace prop and loading flags */} - {props.dataPanel} - {props.workspacePanel} - + {props.dataPanel} + {props.workspacePanel} + {props.configPanel} {props.suggestionsPanel} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx index c8b2ce817b7ae..55e806edbd589 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx @@ -8,12 +8,14 @@ import _ from 'lodash'; import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiButtonIcon, EuiComboBox, EuiPopover, EuiButtonEmpty, + EuiButtonIcon, EuiFlexItem, EuiFlexGroup, + EuiContextMenuItem, + EuiContextMenuPanel, } from '@elastic/eui'; import { DatasourceDimensionPanelProps } from '../types'; import { @@ -37,7 +39,8 @@ export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { }; export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) { - const [isOpen, setOpen] = useState(false); + const [isSettingsOpen, setSettingsOpen] = useState(false); + const [isFieldSelectOpen, setFieldSelectOpen] = useState(false); const operations = getOperations(); const operationPanels = getOperationDisplay(); @@ -54,161 +57,199 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp col => 'sourceField' in col ) as FieldBasedIndexPatternColumn[]; - const uniqueColumnsByField = _.uniq(fieldColumns, col => col.sourceField); + const uniqueColumnsByField = _.uniq( + fieldColumns + .filter(col => selectedColumn && col.operationType === selectedColumn.operationType) + .concat(fieldColumns), + col => col.sourceField + ); const functionsFromField = selectedColumn ? filteredColumns.filter(col => { return ( - !('sourceField' in selectedColumn) || - !('sourceField' in col) || - col.sourceField === selectedColumn.sourceField + (!('sourceField' in selectedColumn) && !('sourceField' in col)) || + ('sourceField' in selectedColumn && + 'sourceField' in col && + col.sourceField === selectedColumn.sourceField) ); }) : filteredColumns; const ParamEditor = - selectedColumn && operationDefinitionMap[selectedColumn.operationType].paramEditor; + selectedColumn && operationDefinitionMap[selectedColumn.operationType].inlineOptions; + const contextOptionBuilder = + selectedColumn && operationDefinitionMap[selectedColumn.operationType].contextMenu; + + const contextOptions = contextOptionBuilder + ? contextOptionBuilder({ + state: props.state, + setState: props.setState, + columnId: props.columnId, + }) + : []; + + const operationMenuItems = operations + .filter(o => selectedColumn && functionsFromField.some(col => col.operationType === o)) + .map(o => ( + { + const newColumn: IndexPatternColumn = filteredColumns.find( + col => + col.operationType === o && + (!('sourceField' in col) || + !('sourceField' in selectedColumn) || + col.sourceField === selectedColumn.sourceField) + )!; + + const newColumns = { + ...props.state.columns, + [props.columnId]: newColumn, + }; + + props.setState({ + ...props.state, + columnOrder: getColumnOrder(newColumns), + columns: newColumns, + }); + }} + > + {operationPanels[o].displayName} + + )); return ( - - - { - setOpen(false); - }} - ownFocus - anchorPosition="rightCenter" - button={ - - { - setOpen(!isOpen); - }} - > - - {selectedColumn - ? selectedColumn.label - : i18n.translate('xpack.lens.indexPattern.configureDimensionLabel', { - defaultMessage: 'Configure dimension', - })} - - - - } - > - - - {(!selectedColumn || 'sourceField' in selectedColumn) && ( - ({ - label: col.sourceField, - value: col.operationId, - }))} - selectedOptions={ - selectedColumn && 'sourceField' in selectedColumn - ? [ - { - label: selectedColumn.sourceField, - value: selectedColumn.operationId, - }, - ] - : [] - } - singleSelection={{ asPlainText: true }} - isClearable={false} - onChange={choices => { - const column: IndexPatternColumn = columns.find( - ({ operationId }) => operationId === choices[0].value - )!; - const newColumns: IndexPatternPrivateState['columns'] = { - ...props.state.columns, - [props.columnId]: column, - }; - - props.setState({ - ...props.state, - columns: newColumns, - columnOrder: getColumnOrder(newColumns), - }); + + + + {!isFieldSelectOpen && + selectedColumn && + (operationMenuItems.length > 1 || contextOptions.length > 0) && ( + + { + setSettingsOpen(false); }} - /> - )} - - {ParamEditor && ( - - + ownFocus + anchorPosition="leftCenter" + panelPaddingSize="none" + button={ + + { + setSettingsOpen(!isSettingsOpen); + }} + iconType="gear" + aria-label="Settings" + /> + + } + > + + {operationMenuItems.concat(contextOptions)} + + )} - -
- {operations.map(o => ( - col.operationType === o)} - onClick={() => { - const newColumn: IndexPatternColumn = filteredColumns.find( - col => - col.operationType === o && - (!('sourceField' in col) || - !('sourceField' in selectedColumn) || - col.sourceField === selectedColumn.sourceField) - )!; - - const newColumns = { - ...props.state.columns, - [props.columnId]: newColumn, - }; - - props.setState({ - ...props.state, - columnOrder: getColumnOrder(newColumns), - columns: newColumns, - }); - }} - > - {operationPanels[o].displayName} - - ))} -
+ + {!isFieldSelectOpen ? ( + setFieldSelectOpen(true)}> + {selectedColumn + ? selectedColumn.label + : i18n.translate('xpack.lens.indexPattern.configureDimensionLabel', { + defaultMessage: 'Configure dimension', + })} + + ) : ( + { + if (el) { + el.focus(); + } + }} + onBlur={() => { + setFieldSelectOpen(false); + }} + data-test-subj="indexPattern-dimension-field" + placeholder="Field" + options={[ + { + label: 'Document', + value: columns.find(column => !('sourceField' in column))!.operationId, + }, + { + label: 'Individual fields', + options: uniqueColumnsByField.map(col => ({ + label: col.sourceField, + value: col.operationId, + })), + }, + ]} + selectedOptions={ + selectedColumn && 'sourceField' in selectedColumn + ? [ + { + label: selectedColumn.sourceField, + value: selectedColumn.operationId, + }, + ] + : [] + } + singleSelection={{ asPlainText: true }} + isClearable={false} + onChange={choices => { + const column: IndexPatternColumn = columns.find( + ({ operationId }) => operationId === choices[0].value + )!; + const newColumns: IndexPatternPrivateState['columns'] = { + ...props.state.columns, + [props.columnId]: column, + }; + + setFieldSelectOpen(false); + props.setState({ + ...props.state, + columns: newColumns, + columnOrder: getColumnOrder(newColumns), + }); + }} + /> + )} + + {selectedColumn && ( + + { + const newColumns: IndexPatternPrivateState['columns'] = { + ...props.state.columns, + }; + delete newColumns[props.columnId]; + + props.setState({ + ...props.state, + columns: newColumns, + columnOrder: getColumnOrder(newColumns), + }); + }} + /> -
- + )} +
- {selectedColumn && ( - - { - const newColumns: IndexPatternPrivateState['columns'] = { - ...props.state.columns, - }; - delete newColumns[props.columnId]; - - props.setState({ - ...props.state, - columns: newColumns, - columnOrder: getColumnOrder(newColumns), - }); - }} - /> + {ParamEditor && ( + + )}
diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index ae4cf2ac7ebe2..df1b9d38f3000 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -31,7 +31,6 @@ export type IndexPatternColumn = | AvgIndexPatternColumn | MinIndexPatternColumn | MaxIndexPatternColumn - | SumIndexPatternColumn | CountIndexPatternColumn; export interface BaseIndexPatternColumn { diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx index 68849545d01a4..dae383a6a79c2 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx @@ -78,7 +78,7 @@ export const dateHistogramOperation: OperationDefinition { + inlineOptions: ({ state, setState, columnId }) => { const column = state.columns[columnId] as DateHistogramIndexPatternColumn; const intervals = ['M', 'w', 'd', 'h']; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts index 7ed064402b6d1..9c00ae68feed3 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts @@ -26,7 +26,7 @@ type PossibleOperationDefinitions< U extends IndexPatternColumn = IndexPatternColumn > = U extends IndexPatternColumn ? OperationDefinition : never; -type PartialOperationDefinitionMap< +type PossibleOperationDefinitionMapEntyries< U extends PossibleOperationDefinitions = PossibleOperationDefinitions > = U extends PossibleOperationDefinitions ? { [K in U['type']]: U } : never; @@ -34,7 +34,7 @@ type UnionToIntersection = (U extends U ? (k: U) => void : never) extends ((k ? I : never; -export type AllOperationDefinitions = UnionToIntersection; +export type AllOperationDefinitions = UnionToIntersection; export const operationDefinitionMap: AllOperationDefinitions = { terms: termsOperation, @@ -64,7 +64,8 @@ export interface OperationDefinition C; - paramEditor?: React.ComponentType; + inlineOptions?: React.ComponentType; + contextMenu?: (props: ParamEditorProps) => JSX.Element[]; toEsAggsConfig: (column: C, columnId: string) => unknown; } diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx index 13071a54d2d4b..be109a9b9a740 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiForm, EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; +import { EuiForm, EuiFormRow, EuiRange, EuiSelect, EuiContextMenuItem } from '@elastic/eui'; import { IndexPatternField, TermsIndexPatternColumn } from '../indexpattern'; import { DimensionPriority } from '../../types'; import { OperationDefinition } from '.'; @@ -77,20 +77,21 @@ export const termsOperation: OperationDefinition = { missingBucketLabel: 'Missing', }, }), - paramEditor: ({ state, setState, columnId: currentColumnId }) => { + contextMenu: ({ state, setState, columnId: currentColumnId }) => { const currentColumn = state.columns[currentColumnId] as TermsIndexPatternColumn; + const SEPARATOR = '$$$'; function toValue(orderBy: TermsIndexPatternColumn['params']['orderBy']) { if (orderBy.type === 'alphabetical') { return orderBy.type; } - return `${orderBy.type}-${orderBy.columnId}`; + return `${orderBy.type}${SEPARATOR}${orderBy.columnId}`; } function fromValue(value: string): TermsIndexPatternColumn['params']['orderBy'] { if (value === 'alphabetical') { return { type: 'alphabetical' }; } - const parts = value.split('-'); + const parts = value.split(SEPARATOR); return { type: 'column', columnId: parts[1], @@ -109,17 +110,13 @@ export const termsOperation: OperationDefinition = { value: toValue({ type: 'alphabetical' }), text: 'Alphabetical', }); - - return ( - - - ) => + return [ + + + ) => setState({ ...state, columns: { @@ -128,20 +125,29 @@ export const termsOperation: OperationDefinition = { ...currentColumn, params: { ...currentColumn.params, - size: Number(e.target.value), + orderBy: fromValue(e.target.value), }, }, }, }) } - aria-label="Number of values" /> - - ) => + , + ]; + }, + inlineOptions: ({ state, setState, columnId: currentColumnId }) => { + const currentColumn = state.columns[currentColumnId] as TermsIndexPatternColumn; + return ( + + + ) => setState({ ...state, columns: { @@ -150,12 +156,13 @@ export const termsOperation: OperationDefinition = { ...currentColumn, params: { ...currentColumn.params, - orderBy: fromValue(e.target.value), + size: Number(e.target.value), }, }, }, }) } + aria-label="Number of values" /> From 520f0e725083b2c99821af6f74fe43ae48612bc0 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 12 Jun 2019 17:02:43 +0200 Subject: [PATCH 13/27] split up dimension panel code --- .../indexpattern_plugin/dimension_panel.tsx | 257 ------------------ .../dimension_panel.test.tsx | 0 .../dimension_panel/dimension_panel.tsx | 53 ++++ .../dimension_panel/field_select.tsx | 135 +++++++++ .../dimension_panel/index.ts | 7 + .../dimension_panel/settings.tsx | 121 +++++++++ .../operations/date_histogram.tsx | 22 +- .../indexpattern_plugin/operations/terms.tsx | 31 +-- .../indexpattern_plugin/operations/utils.ts | 43 +++ .../indexpattern_plugin/to_expression.ts | 10 +- 10 files changed, 380 insertions(+), 299 deletions(-) delete mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx rename x-pack/plugins/lens/public/indexpattern_plugin/{ => dimension_panel}/dimension_panel.test.tsx (100%) create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/index.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operations/utils.ts diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx deleted file mode 100644 index 55e806edbd589..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx +++ /dev/null @@ -1,257 +0,0 @@ -/* - * 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 _ from 'lodash'; -import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiComboBox, - EuiPopover, - EuiButtonEmpty, - EuiButtonIcon, - EuiFlexItem, - EuiFlexGroup, - EuiContextMenuItem, - EuiContextMenuPanel, -} from '@elastic/eui'; -import { DatasourceDimensionPanelProps } from '../types'; -import { - IndexPatternColumn, - IndexPatternPrivateState, - columnToOperation, - FieldBasedIndexPatternColumn, -} from './indexpattern'; - -import { - getOperationDisplay, - getOperations, - getPotentialColumns, - getColumnOrder, - operationDefinitionMap, -} from './operations'; - -export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { - state: IndexPatternPrivateState; - setState: (newState: IndexPatternPrivateState) => void; -}; - -export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) { - const [isSettingsOpen, setSettingsOpen] = useState(false); - const [isFieldSelectOpen, setFieldSelectOpen] = useState(false); - - const operations = getOperations(); - const operationPanels = getOperationDisplay(); - - const columns = getPotentialColumns(props.state, props.suggestedPriority); - - const filteredColumns = columns.filter(col => { - return props.filterOperations(columnToOperation(col)); - }); - - const selectedColumn: IndexPatternColumn | null = props.state.columns[props.columnId] || null; - - const fieldColumns = filteredColumns.filter( - col => 'sourceField' in col - ) as FieldBasedIndexPatternColumn[]; - - const uniqueColumnsByField = _.uniq( - fieldColumns - .filter(col => selectedColumn && col.operationType === selectedColumn.operationType) - .concat(fieldColumns), - col => col.sourceField - ); - - const functionsFromField = selectedColumn - ? filteredColumns.filter(col => { - return ( - (!('sourceField' in selectedColumn) && !('sourceField' in col)) || - ('sourceField' in selectedColumn && - 'sourceField' in col && - col.sourceField === selectedColumn.sourceField) - ); - }) - : filteredColumns; - - const ParamEditor = - selectedColumn && operationDefinitionMap[selectedColumn.operationType].inlineOptions; - const contextOptionBuilder = - selectedColumn && operationDefinitionMap[selectedColumn.operationType].contextMenu; - - const contextOptions = contextOptionBuilder - ? contextOptionBuilder({ - state: props.state, - setState: props.setState, - columnId: props.columnId, - }) - : []; - - const operationMenuItems = operations - .filter(o => selectedColumn && functionsFromField.some(col => col.operationType === o)) - .map(o => ( - { - const newColumn: IndexPatternColumn = filteredColumns.find( - col => - col.operationType === o && - (!('sourceField' in col) || - !('sourceField' in selectedColumn) || - col.sourceField === selectedColumn.sourceField) - )!; - - const newColumns = { - ...props.state.columns, - [props.columnId]: newColumn, - }; - - props.setState({ - ...props.state, - columnOrder: getColumnOrder(newColumns), - columns: newColumns, - }); - }} - > - {operationPanels[o].displayName} - - )); - - return ( - - - - {!isFieldSelectOpen && - selectedColumn && - (operationMenuItems.length > 1 || contextOptions.length > 0) && ( - - { - setSettingsOpen(false); - }} - ownFocus - anchorPosition="leftCenter" - panelPaddingSize="none" - button={ - - { - setSettingsOpen(!isSettingsOpen); - }} - iconType="gear" - aria-label="Settings" - /> - - } - > - - {operationMenuItems.concat(contextOptions)} - - - - )} - - {!isFieldSelectOpen ? ( - setFieldSelectOpen(true)}> - {selectedColumn - ? selectedColumn.label - : i18n.translate('xpack.lens.indexPattern.configureDimensionLabel', { - defaultMessage: 'Configure dimension', - })} - - ) : ( - { - if (el) { - el.focus(); - } - }} - onBlur={() => { - setFieldSelectOpen(false); - }} - data-test-subj="indexPattern-dimension-field" - placeholder="Field" - options={[ - { - label: 'Document', - value: columns.find(column => !('sourceField' in column))!.operationId, - }, - { - label: 'Individual fields', - options: uniqueColumnsByField.map(col => ({ - label: col.sourceField, - value: col.operationId, - })), - }, - ]} - selectedOptions={ - selectedColumn && 'sourceField' in selectedColumn - ? [ - { - label: selectedColumn.sourceField, - value: selectedColumn.operationId, - }, - ] - : [] - } - singleSelection={{ asPlainText: true }} - isClearable={false} - onChange={choices => { - const column: IndexPatternColumn = columns.find( - ({ operationId }) => operationId === choices[0].value - )!; - const newColumns: IndexPatternPrivateState['columns'] = { - ...props.state.columns, - [props.columnId]: column, - }; - - setFieldSelectOpen(false); - props.setState({ - ...props.state, - columns: newColumns, - columnOrder: getColumnOrder(newColumns), - }); - }} - /> - )} - - {selectedColumn && ( - - { - const newColumns: IndexPatternPrivateState['columns'] = { - ...props.state.columns, - }; - delete newColumns[props.columnId]; - - props.setState({ - ...props.state, - columns: newColumns, - columnOrder: getColumnOrder(newColumns), - }); - }} - /> - - )} - - - {ParamEditor && ( - - - - )} - - ); -} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx similarity index 100% rename from x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx rename to x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx new file mode 100644 index 0000000000000..16b0ff035e9ae --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -0,0 +1,53 @@ +/* + * 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 _ from 'lodash'; +import React from 'react'; +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { DatasourceDimensionPanelProps } from '../../types'; +import { IndexPatternColumn, IndexPatternPrivateState, columnToOperation } from '../indexpattern'; + +import { getPotentialColumns, operationDefinitionMap } from '../operations'; +import { FieldSelect } from './field_select'; +import { Settings } from './settings'; + +export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { + state: IndexPatternPrivateState; + setState: (newState: IndexPatternPrivateState) => void; +}; + +export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) { + const columns = getPotentialColumns(props.state, props.suggestedPriority); + + const filteredColumns = columns.filter(col => { + return props.filterOperations(columnToOperation(col)); + }); + + const selectedColumn: IndexPatternColumn | null = props.state.columns[props.columnId] || null; + + const ParamEditor = + selectedColumn && operationDefinitionMap[selectedColumn.operationType].inlineOptions; + + return ( + + + + + + + + {ParamEditor && ( + + + + )} + + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx new file mode 100644 index 0000000000000..e9f75a0157ae2 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx @@ -0,0 +1,135 @@ +/* + * 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 _ from 'lodash'; +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiComboBox, EuiButtonEmpty, EuiButtonIcon, EuiFlexItem } from '@elastic/eui'; +import { + IndexPatternColumn, + FieldBasedIndexPatternColumn, + IndexPatternPrivateState, +} from '../indexpattern'; +import { IndexPatternDimensionPanelProps } from './dimension_panel'; +import { getColumnOrder } from '../operations'; + +export interface FieldSelectProps extends IndexPatternDimensionPanelProps { + selectedColumn: IndexPatternColumn; + filteredColumns: IndexPatternColumn[]; +} + +export function FieldSelect({ + selectedColumn, + filteredColumns, + state, + columnId, + setState, +}: FieldSelectProps) { + const [isFieldSelectOpen, setFieldSelectOpen] = useState(false); + const fieldColumns = filteredColumns.filter( + col => 'sourceField' in col + ) as FieldBasedIndexPatternColumn[]; + + const uniqueColumnsByField = _.uniq( + fieldColumns + .filter(col => selectedColumn && col.operationType === selectedColumn.operationType) + .concat(fieldColumns), + col => col.sourceField + ); + return ( + <> + + {!isFieldSelectOpen ? ( + setFieldSelectOpen(true)}> + {selectedColumn + ? selectedColumn.label + : i18n.translate('xpack.lens.indexPattern.configureDimensionLabel', { + defaultMessage: 'Configure dimension', + })} + + ) : ( + { + if (el) { + el.focus(); + } + }} + onBlur={() => { + setFieldSelectOpen(false); + }} + data-test-subj="indexPattern-dimension-field" + placeholder="Field" + options={[ + { + label: 'Document', + value: filteredColumns.find(column => !('sourceField' in column))!.operationId, + }, + { + label: 'Individual fields', + options: uniqueColumnsByField.map(col => ({ + label: col.sourceField, + value: col.operationId, + })), + }, + ]} + selectedOptions={ + selectedColumn && 'sourceField' in selectedColumn + ? [ + { + label: selectedColumn.sourceField, + value: selectedColumn.operationId, + }, + ] + : [] + } + singleSelection={{ asPlainText: true }} + isClearable={false} + onChange={choices => { + const column: IndexPatternColumn = filteredColumns.find( + ({ operationId }) => operationId === choices[0].value + )!; + const newColumns: IndexPatternPrivateState['columns'] = { + ...state.columns, + [columnId]: column, + }; + + setFieldSelectOpen(false); + setState({ + ...state, + columns: newColumns, + columnOrder: getColumnOrder(newColumns), + }); + }} + /> + )} + + {selectedColumn && ( + + { + const newColumns: IndexPatternPrivateState['columns'] = { + ...state.columns, + }; + delete newColumns[columnId]; + + setState({ + ...state, + columns: newColumns, + columnOrder: getColumnOrder(newColumns), + }); + }} + /> + + )} + + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/index.ts b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/index.ts new file mode 100644 index 0000000000000..88e5588ce0e01 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export * from './dimension_panel'; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx new file mode 100644 index 0000000000000..752bf95a5c851 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx @@ -0,0 +1,121 @@ +/* + * 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 _ from 'lodash'; +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPopover, + EuiButtonIcon, + EuiFlexItem, + EuiContextMenuItem, + EuiContextMenuPanel, +} from '@elastic/eui'; +import { IndexPatternColumn } from '../indexpattern'; +import { IndexPatternDimensionPanelProps } from './dimension_panel'; +import { + getColumnOrder, + operationDefinitionMap, + getOperations, + getOperationDisplay, +} from '../operations'; + +export interface SettingsProps extends IndexPatternDimensionPanelProps { + selectedColumn: IndexPatternColumn; + filteredColumns: IndexPatternColumn[]; +} + +export function Settings({ + selectedColumn, + filteredColumns, + state, + columnId, + setState, +}: SettingsProps) { + const [isSettingsOpen, setSettingsOpen] = useState(false); + const contextOptionBuilder = + selectedColumn && operationDefinitionMap[selectedColumn.operationType].contextMenu; + const contextOptions = contextOptionBuilder + ? contextOptionBuilder({ + state, + setState, + columnId, + }) + : []; + const operations = getOperations(); + const operationPanels = getOperationDisplay(); + const functionsFromField = selectedColumn + ? filteredColumns.filter(col => { + return ( + (!('sourceField' in selectedColumn) && !('sourceField' in col)) || + ('sourceField' in selectedColumn && + 'sourceField' in col && + col.sourceField === selectedColumn.sourceField) + ); + }) + : filteredColumns; + + const operationMenuItems = operations + .filter(o => selectedColumn && functionsFromField.some(col => col.operationType === o)) + .map(o => ( + { + const newColumn: IndexPatternColumn = filteredColumns.find( + col => + col.operationType === o && + (!('sourceField' in col) || + !('sourceField' in selectedColumn) || + col.sourceField === selectedColumn.sourceField) + )!; + + const newColumns = { + ...state.columns, + [columnId]: newColumn, + }; + + setState({ + ...state, + columnOrder: getColumnOrder(newColumns), + columns: newColumns, + }); + }} + > + {operationPanels[o].displayName} + + )); + + return selectedColumn && (operationMenuItems.length > 1 || contextOptions.length > 0) ? ( + + { + setSettingsOpen(false); + }} + ownFocus + anchorPosition="leftCenter" + panelPaddingSize="none" + button={ + + { + setSettingsOpen(!isSettingsOpen); + }} + iconType="gear" + aria-label="Settings" + /> + + } + > + {operationMenuItems.concat(contextOptions)} + + + ) : null; +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx index dae383a6a79c2..1fab9a0d6c9e2 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx @@ -10,6 +10,7 @@ import { EuiForm, EuiFormRow, EuiRange } from '@elastic/eui'; import { IndexPatternField, DateHistogramIndexPatternColumn } from '../indexpattern'; import { DimensionPriority } from '../../types'; import { OperationDefinition } from '.'; +import { updateColumnParam } from './utils'; type PropType = C extends React.ComponentType ? P : unknown; @@ -100,19 +101,14 @@ export const dateHistogramOperation: OperationDefinition ({ label: interval, value: index }))} onChange={(e: React.ChangeEvent) => - setState({ - ...state, - columns: { - ...state.columns, - [columnId]: { - ...column, - params: { - ...column.params, - interval: numericToInterval(Number(e.target.value)), - }, - }, - }, - }) + setState( + updateColumnParam( + state, + column, + 'interval', + numericToInterval(Number(e.target.value)) + ) + ) } aria-label="Level of Detail" /> diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx index be109a9b9a740..8da0afbb7eb81 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx @@ -10,6 +10,7 @@ import { EuiForm, EuiFormRow, EuiRange, EuiSelect, EuiContextMenuItem } from '@e import { IndexPatternField, TermsIndexPatternColumn } from '../indexpattern'; import { DimensionPriority } from '../../types'; import { OperationDefinition } from '.'; +import { updateColumnParam } from './utils'; type PropType = C extends React.ComponentType ? P : unknown; @@ -117,19 +118,9 @@ export const termsOperation: OperationDefinition = { options={orderOptions} value={toValue(currentColumn.params.orderBy)} onChange={(e: React.ChangeEvent) => - setState({ - ...state, - columns: { - ...state.columns, - [currentColumnId]: { - ...currentColumn, - params: { - ...currentColumn.params, - orderBy: fromValue(e.target.value), - }, - }, - }, - }) + setState( + updateColumnParam(state, currentColumn, 'orderBy', fromValue(e.target.value)) + ) } /> @@ -148,19 +139,7 @@ export const termsOperation: OperationDefinition = { value={currentColumn.params.size} showInput onChange={(e: React.ChangeEvent) => - setState({ - ...state, - columns: { - ...state.columns, - [currentColumnId]: { - ...currentColumn, - params: { - ...currentColumn.params, - size: Number(e.target.value), - }, - }, - }, - }) + setState(updateColumnParam(state, currentColumn, 'size', Number(e.target.value))) } aria-label="Number of values" /> diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/utils.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations/utils.ts new file mode 100644 index 0000000000000..d0a6c08db9d18 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/utils.ts @@ -0,0 +1,43 @@ +/* + * 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 { + IndexPatternPrivateState, + IndexPatternColumn, + BaseIndexPatternColumn, +} from '../indexpattern'; + +export function updateColumnParam< + C extends BaseIndexPatternColumn & { params: object }, + K extends keyof C['params'] +>( + state: IndexPatternPrivateState, + currentColumn: C, + paramName: K, + value: C['params'][K] +): IndexPatternPrivateState { + const columnId = Object.entries(state.columns).find( + ([_, column]) => column === currentColumn + )![0]; + + if (!('params' in state.columns[columnId])) { + throw new Error('Invariant: no params in this column'); + } + + return { + ...state, + columns: { + ...state.columns, + [columnId]: ({ + ...currentColumn, + params: { + ...currentColumn.params, + [paramName]: value, + }, + } as unknown) as IndexPatternColumn, + }, + }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts index 8d351ed5d60b2..8450031268d5f 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -6,7 +6,11 @@ import _ from 'lodash'; -import { IndexPatternPrivateState, IndexPatternColumn } from './indexpattern'; +import { + IndexPatternPrivateState, + IndexPatternColumn, + ValueIndexPatternColumn, +} from './indexpattern'; import { operationDefinitionMap, OperationDefinition } from './operations'; export function toExpression(state: IndexPatternPrivateState) { @@ -28,8 +32,8 @@ export function toExpression(state: IndexPatternPrivateState) { } if (sortedColumns.every(({ operationType }) => operationType === 'value')) { - const fieldNames = sortedColumns.map(column => - 'sourceField' in column ? column.sourceField : undefined + const fieldNames = (sortedColumns as ValueIndexPatternColumn[]).map( + column => column.sourceField ); return `esdocs index="${indexName}" fields="${fieldNames.join(', ')}" sort="${ From cdcc8223694f5a61105a747c3380339364986a12 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 13 Jun 2019 09:08:11 +0200 Subject: [PATCH 14/27] fix some tests --- .../dimension_panel.test.tsx.snap | 191 ------------------ .../dimension_panel/dimension_panel.test.tsx | 23 +-- .../indexpattern_plugin/indexpattern.test.tsx | 39 +--- .../indexpattern_plugin/indexpattern.tsx | 2 - .../indexpattern_plugin/operations/index.ts | 9 +- .../operations/metrics.tsx | 2 +- .../{ => operations}/operations.test.ts | 81 ++++---- .../indexpattern_plugin/operations/value.tsx | 49 ----- .../indexpattern_plugin/to_expression.ts | 25 +-- 9 files changed, 68 insertions(+), 353 deletions(-) delete mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap rename x-pack/plugins/lens/public/indexpattern_plugin/{ => operations}/operations.test.ts (84%) delete mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operations/value.tsx diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap deleted file mode 100644 index 770c5409eab6b..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/dimension_panel.test.tsx.snap +++ /dev/null @@ -1,191 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`IndexPatternDimensionPanel should render a dimension panel 1`] = ` - - - - - - Configure dimension - - - - } - closePopover={[Function]} - hasArrow={true} - id="col2" - isOpen={false} - ownFocus={true} - panelClassName="lnsIndexPattern__dimensionPopover" - panelPaddingSize="m" - > - - - - - -
- - - Value - - - - - Top Values - - - - - Date Histogram - - - - - Sum - - - - - Average - - - - - Minimum - - - - - Maximum - - - - - Count - - -
-
-
- - -
-`; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index 2e1fb83a90571..939054a337902 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -7,11 +7,11 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { EuiComboBox } from '@elastic/eui'; -import { IndexPatternPrivateState } from './indexpattern'; -import { getColumnOrder, getPotentialColumns } from './operations'; +import { IndexPatternPrivateState } from '../indexpattern'; +import { getColumnOrder, getPotentialColumns } from '../operations'; import { IndexPatternDimensionPanel } from './dimension_panel'; -jest.mock('./operations'); +jest.mock('../operations'); const expectedIndexPatterns = { 1: { @@ -57,7 +57,10 @@ describe('IndexPatternDimensionPanel', () => { isBucketed: false, // Private - operationType: 'value', + operationType: 'date_histogram', + params: { + interval: '1d', + }, sourceField: 'timestamp', }, }, @@ -157,12 +160,6 @@ describe('IndexPatternDimensionPanel', () => { /> ); - expect( - wrapper.find('[data-test-subj="lns-indexPatternDimension-value"]').prop('color') - ).toEqual('primary'); - expect( - wrapper.find('[data-test-subj="lns-indexPatternDimension-value"]').prop('isDisabled') - ).toEqual(false); expect( wrapper.find('[data-test-subj="lns-indexPatternDimension-terms"]').prop('isDisabled') ).toEqual(true); @@ -180,7 +177,7 @@ describe('IndexPatternDimensionPanel', () => { ).toEqual(true); }); - it('should update the datasource state on selection of a value operation', () => { + it('should update the datasource state on selection of an operation', () => { const setState = jest.fn(); const wrapper = shallow( @@ -257,10 +254,10 @@ describe('IndexPatternDimensionPanel', () => { ...state.columns, col2: expect.objectContaining({ operationId: firstField.value, - label: 'Value of bytes', + label: 'Average of bytes', dataType: 'number', isBucketed: false, - operationType: 'value', + operationType: 'avg', sourceField: 'bytes', }), }, diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index bc402263544aa..41c167410f0aa 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -121,8 +121,12 @@ describe('IndexPattern Data Source', () => { isBucketed: false, // Private - operationType: 'value', + operationType: 'terms', sourceField: 'op', + params: { + size: 5, + orderBy: { type: 'alphabetical' }, + }, }, }, }; @@ -196,39 +200,6 @@ describe('IndexPattern Data Source', () => { expect(indexPatternDatasource.toExpression(state)).toEqual(null); }); - it('should generate an expression for a values query', async () => { - const queryPersistedState: IndexPatternPersistedState = { - currentIndexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: { - operationId: 'op1', - label: 'My Op', - dataType: 'string', - isBucketed: false, - - // Private - operationType: 'value', - sourceField: 'source', - }, - col2: { - operationId: 'op2', - label: 'My Op 2', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'value', - sourceField: 'bytes', - }, - }, - }; - const state = await indexPatternDatasource.initialize(queryPersistedState); - expect(indexPatternDatasource.toExpression(state)).toMatchInlineSnapshot( - `"esdocs index=\\"my-fake-index-pattern\\" fields=\\"source, bytes\\" sort=\\"source, DESC\\" | lens_rename_columns idMap='{\\"source\\":\\"col1\\",\\"bytes\\":\\"col2\\"}'"` - ); - }); - it('should generate an expression for an aggregated query', async () => { const queryPersistedState: IndexPatternPersistedState = { currentIndexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index df1b9d38f3000..9e0b3d410f053 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -26,7 +26,6 @@ export type OperationType = IndexPatternColumn['operationType']; export type IndexPatternColumn = | DateHistogramIndexPatternColumn | TermsIndexPatternColumn - | ValueIndexPatternColumn | SumIndexPatternColumn | AvgIndexPatternColumn | MinIndexPatternColumn @@ -71,7 +70,6 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { }; } -export type ValueIndexPatternColumn = ParameterlessIndexPatternColumn<'value'>; export type CountIndexPatternColumn = ParameterlessIndexPatternColumn< 'count', BaseIndexPatternColumn diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts index 9c00ae68feed3..959e1c8e96b76 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts @@ -16,12 +16,8 @@ import { import { termsOperation } from './terms'; import { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; import { dateHistogramOperation } from './date_histogram'; -import { valueOperation } from './value'; import { countOperation } from './count'; -export function getOperations(): OperationType[] { - return ['value', 'terms', 'date_histogram', 'sum', 'avg', 'min', 'max', 'count']; -} type PossibleOperationDefinitions< U extends IndexPatternColumn = IndexPatternColumn > = U extends IndexPatternColumn ? OperationDefinition : never; @@ -43,11 +39,14 @@ export const operationDefinitionMap: AllOperationDefinitions = { max: maxOperation, avg: averageOperation, sum: sumOperation, - value: valueOperation, count: countOperation, }; const operationDefinitions: PossibleOperationDefinitions[] = Object.values(operationDefinitionMap); +export function getOperations(): OperationType[] { + return Object.keys(operationDefinitionMap) as OperationType[]; +} + export interface ParamEditorProps { state: IndexPatternPrivateState; setState: (newState: IndexPatternPrivateState) => void; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx index 3e4a834416f59..9e90dbd1d274f 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx @@ -27,7 +27,7 @@ function buildMetricOperation( isApplicableWithoutField: false, isApplicableForField: ({ aggregationRestrictions, type: fieldType }: IndexPatternField) => { return Boolean( - fieldType === 'number' && (!aggregationRestrictions || aggregationRestrictions[fieldType]) + fieldType === 'number' && (!aggregationRestrictions || aggregationRestrictions[type]) ); }, buildColumn( diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts similarity index 84% rename from x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts rename to x-pack/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts index 66aaa3660c76d..4732281105c77 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOperationTypesForField, getPotentialColumns, getColumnOrder } from './operations'; -import { IndexPatternPrivateState } from './indexpattern'; +import { getOperationTypesForField, getPotentialColumns, getColumnOrder } from '.'; +import { IndexPatternPrivateState } from '../indexpattern'; const expectedIndexPatterns = { 1: { @@ -45,7 +45,7 @@ describe('getOperationTypesForField', () => { aggregatable: true, searchable: true, }) - ).toEqual(expect.arrayContaining(['value', 'terms'])); + ).toEqual(expect.arrayContaining(['terms'])); }); it('should return operations on numbers', () => { @@ -56,7 +56,7 @@ describe('getOperationTypesForField', () => { aggregatable: true, searchable: true, }) - ).toEqual(expect.arrayContaining(['value', 'avg', 'sum', 'min', 'max'])); + ).toEqual(expect.arrayContaining(['avg', 'sum', 'min', 'max'])); }); it('should return operations on dates', () => { @@ -67,7 +67,7 @@ describe('getOperationTypesForField', () => { aggregatable: true, searchable: true, }) - ).toEqual(expect.arrayContaining(['value', 'date_histogram'])); + ).toEqual(expect.arrayContaining(['date_histogram'])); }); it('should return no operations on unknown types', () => { @@ -121,7 +121,7 @@ describe('getOperationTypesForField', () => { it('should return operations on dates', () => { expect( getOperationTypesForField({ - type: 'dates', + type: 'date', name: 'a', aggregatable: true, searchable: true, @@ -154,8 +154,11 @@ describe('getOperationTypesForField', () => { isBucketed: false, // Private - operationType: 'value', + operationType: 'date_histogram', sourceField: 'timestamp', + params: { + interval: 'h', + }, }, }, }; @@ -170,44 +173,37 @@ describe('getOperationTypesForField', () => { it('should list operations by field for a regular index pattern', () => { const columns = getPotentialColumns(state); - expect(columns.map(col => [('sourceField' in col) ? col.sourceField : '_documents_', col.operationType])).toMatchInlineSnapshot(` + expect( + columns.map(col => [ + 'sourceField' in col ? col.sourceField : '_documents_', + col.operationType, + ]) + ).toMatchInlineSnapshot(` Array [ Array [ "bytes", - "value", + "min", ], Array [ "bytes", - "sum", + "max", ], Array [ "bytes", "avg", ], - Array [ - "bytes", - "min", - ], - Array [ - "bytes", - "max", - ], Array [ "_documents_", "count", ], Array [ - "source", - "value", + "bytes", + "sum", ], Array [ "source", "terms", ], - Array [ - "timestamp", - "value", - ], Array [ "timestamp", "date_histogram", @@ -233,8 +229,11 @@ describe('getColumnOrder', () => { isBucketed: false, // Private - operationType: 'value', + operationType: 'date_histogram', sourceField: 'timestamp', + params: { + interval: 'h', + }, }, }) ).toEqual(['col1']); @@ -250,8 +249,14 @@ describe('getColumnOrder', () => { isBucketed: true, // Private - operationType: 'value', - sourceField: 'timestamp', + operationType: 'terms', + sourceField: 'category', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + }, }, col2: { operationId: 'op2', @@ -260,7 +265,7 @@ describe('getColumnOrder', () => { isBucketed: false, // Private - operationType: 'value', + operationType: 'avg', sourceField: 'bytes', }, col3: { @@ -273,8 +278,8 @@ describe('getColumnOrder', () => { operationType: 'date_histogram', sourceField: 'timestamp', params: { - interval: '1d' - } + interval: '1d', + }, }, }) ).toEqual(['col1', 'col3', 'col2']); @@ -290,8 +295,14 @@ describe('getColumnOrder', () => { isBucketed: true, // Private - operationType: 'value', - sourceField: 'timestamp', + operationType: 'terms', + sourceField: 'category', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + }, suggestedOrder: 2, }, col2: { @@ -301,7 +312,7 @@ describe('getColumnOrder', () => { isBucketed: false, // Private - operationType: 'value', + operationType: 'avg', sourceField: 'bytes', suggestedOrder: 0, }, @@ -316,8 +327,8 @@ describe('getColumnOrder', () => { sourceField: 'timestamp', suggestedOrder: 1, params: { - interval: '1d' - } + interval: '1d', + }, }, }) ).toEqual(['col3', 'col1', 'col2']); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/value.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operations/value.tsx deleted file mode 100644 index 399284980abf3..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/value.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import { IndexPatternField, ValueIndexPatternColumn } from '../indexpattern'; -import { DataType, DimensionPriority } from '../../types'; -import { OperationDefinition } from '.'; - -function ofName(name: string) { - return i18n.translate('xpack.lens.indexPattern.valueOf', { - defaultMessage: 'Value of {name}', - values: { name }, - }); -} - -export const valueOperation: OperationDefinition = { - type: 'value', - displayName: i18n.translate('xpack.lens.indexPattern.value', { - defaultMessage: 'Value', - }), - isApplicableWithoutField: false, - isApplicableForField: ({ aggregationRestrictions, type }) => { - return !aggregationRestrictions; - }, - buildColumn( - operationId: string, - suggestedOrder?: DimensionPriority, - field?: IndexPatternField - ): ValueIndexPatternColumn { - if (!field) { - throw new Error('Invariant: value operation is only valid on a field'); - } - return { - operationId, - label: ofName(field ? field.name : ''), - dataType: field.type as DataType, - operationType: 'value', - suggestedOrder, - sourceField: field.name, - isBucketed: false, - }; - }, - toEsAggsConfig() { - throw new Error('Invariant: The value operation does not have an esaggs config'); - }, -}; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts index ef971e0c51847..6f5b78a714063 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -6,11 +6,7 @@ import _ from 'lodash'; -import { - IndexPatternPrivateState, - IndexPatternColumn, - ValueIndexPatternColumn, -} from './indexpattern'; +import { IndexPatternPrivateState, IndexPatternColumn } from './indexpattern'; import { operationDefinitionMap, OperationDefinition } from './operations'; export function toExpression(state: IndexPatternPrivateState) { @@ -20,8 +16,6 @@ export function toExpression(state: IndexPatternPrivateState) { const sortedColumns = state.columnOrder.map(col => state.columns[col]); - const indexName = state.indexPatterns[state.currentIndexPatternId].title; - function getEsAggsConfig(column: C, columnId: string) { // Typescript is not smart enough to infer that definitionMap[C['operationType']] is always OperationDefinition, // but this is made sure by the typing in operations/index.ts @@ -31,22 +25,7 @@ export function toExpression(state: IndexPatternPrivateState) { return operationDefinition.toEsAggsConfig(column, columnId); } - if (sortedColumns.every(({ operationType }) => operationType === 'value')) { - const fieldNames = (sortedColumns as ValueIndexPatternColumn[]).map( - column => column.sourceField - ); - - const idMap = fieldNames.reduce( - (currentIdMap, fieldName, index) => ({ - ...currentIdMap, - [fieldName]: state.columnOrder[index], - }), - {} as Record - ); - return `esdocs index="${indexName}" fields="${fieldNames.join(', ')}" sort="${ - fieldNames[0] - }, DESC" | lens_rename_columns idMap='${JSON.stringify(idMap)}'`; - } else if (sortedColumns.length) { + if (sortedColumns.length) { const aggs = sortedColumns.map((col, index) => { return getEsAggsConfig(col, state.columnOrder[index]); }); From b72998dd4d0cda53cfcb47fbcafa301cca5c6fc6 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 13 Jun 2019 09:27:46 +0200 Subject: [PATCH 15/27] moved stuff around and started cleaning up and adding tests --- .../indexpattern_plugin/__mocks__/operations.ts | 1 + .../dimension_panel/dimension_panel.test.tsx | 8 ++++---- .../dimension_panel/field_select.tsx | 5 ++++- .../count.tsx | 2 +- .../date_histogram.tsx | 2 +- .../metrics.tsx | 2 +- .../terms.tsx | 2 +- .../utils.ts | 0 .../{operations => }/operations.test.ts | 4 ++-- .../{operations/index.ts => operations.ts} | 17 +++++++++++------ 10 files changed, 26 insertions(+), 17 deletions(-) rename x-pack/plugins/lens/public/indexpattern_plugin/{operations => operation_definitions}/count.tsx (95%) rename x-pack/plugins/lens/public/indexpattern_plugin/{operations => operation_definitions}/date_histogram.tsx (98%) rename x-pack/plugins/lens/public/indexpattern_plugin/{operations => operation_definitions}/metrics.tsx (98%) rename x-pack/plugins/lens/public/indexpattern_plugin/{operations => operation_definitions}/terms.tsx (98%) rename x-pack/plugins/lens/public/indexpattern_plugin/{operations => operation_definitions}/utils.ts (100%) rename x-pack/plugins/lens/public/indexpattern_plugin/{operations => }/operations.test.ts (98%) rename x-pack/plugins/lens/public/indexpattern_plugin/{operations/index.ts => operations.ts} (92%) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts index 0d7fcdecbc340..e0ad308cb3032 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts @@ -16,4 +16,5 @@ export const { getOperationDisplay, getOperationTypesForField, getOperationResultType, + operationDefinitionMap, } = actual; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index 5666809b54ee4..307faa2eb9c43 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -8,9 +8,9 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { EuiComboBox } from '@elastic/eui'; import { IndexPatternPrivateState } from '../indexpattern'; -import { getColumnOrder, getPotentialColumns } from '../operations'; +import { getPotentialColumns, getColumnOrder } from '../operations'; import { IndexPatternDimensionPanel } from './dimension_panel'; -import { DragContextState, DropHandler } from '../../drag_drop'; +import { DropHandler, DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; jest.mock('../operations'); @@ -74,7 +74,7 @@ describe('IndexPatternDimensionPanel', () => { jest.clearAllMocks(); }); - it('should display a call to action in the popover button', () => { + it('should display a configure button if dimension has no column yet', () => { const wrapper = mount( { ); expect( wrapper - .find('[data-test-subj="indexPattern-dimensionPopover-button"]') + .find('[data-test-subj="indexPattern-configure-dimension"]') .first() .text() ).toEqual('Configure dimension'); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx index e9f75a0157ae2..6b02775cc2118 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx @@ -43,7 +43,10 @@ export function FieldSelect({ <> {!isFieldSelectOpen ? ( - setFieldSelectOpen(true)}> + setFieldSelectOpen(true)} + > {selectedColumn ? selectedColumn.label : i18n.translate('xpack.lens.indexPattern.configureDimensionLabel', { diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/count.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx similarity index 95% rename from x-pack/plugins/lens/public/indexpattern_plugin/operations/count.tsx rename to x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx index b9040450f3897..d37504ad32fe5 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CountIndexPatternColumn } from '../indexpattern'; import { DimensionPriority } from '../../types'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from '../operations'; export const countOperation: OperationDefinition = { type: 'count', diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx similarity index 98% rename from x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx rename to x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx index 1fab9a0d6c9e2..c9aefe86d4448 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiForm, EuiFormRow, EuiRange } from '@elastic/eui'; import { IndexPatternField, DateHistogramIndexPatternColumn } from '../indexpattern'; import { DimensionPriority } from '../../types'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from '../operations'; import { updateColumnParam } from './utils'; type PropType = C extends React.ComponentType ? P : unknown; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx similarity index 98% rename from x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx rename to x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx index 9e90dbd1d274f..4e5bb97276e20 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx @@ -14,7 +14,7 @@ import { MaxIndexPatternColumn, } from '../indexpattern'; import { DimensionPriority } from '../../types'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from '../operations'; function buildMetricOperation( type: T['operationType'], diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx similarity index 98% rename from x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx rename to x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx index 8da0afbb7eb81..39879c2f8ad11 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiForm, EuiFormRow, EuiRange, EuiSelect, EuiContextMenuItem } from '@elastic/eui'; import { IndexPatternField, TermsIndexPatternColumn } from '../indexpattern'; import { DimensionPriority } from '../../types'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from '../operations'; import { updateColumnParam } from './utils'; type PropType = C extends React.ComponentType ? P : unknown; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/utils.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.ts similarity index 100% rename from x-pack/plugins/lens/public/indexpattern_plugin/operations/utils.ts rename to x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.ts diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts similarity index 98% rename from x-pack/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts rename to x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts index 4732281105c77..7399a97d110cb 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOperationTypesForField, getPotentialColumns, getColumnOrder } from '.'; -import { IndexPatternPrivateState } from '../indexpattern'; +import { getOperationTypesForField, getPotentialColumns, getColumnOrder } from './operations'; +import { IndexPatternPrivateState } from './indexpattern'; const expectedIndexPatterns = { 1: { diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts similarity index 92% rename from x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts rename to x-pack/plugins/lens/public/indexpattern_plugin/operations.ts index 959e1c8e96b76..59d4d795de969 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts @@ -5,18 +5,23 @@ */ import _ from 'lodash'; -import { DimensionPriority } from '../../types'; +import { DimensionPriority } from '../types'; import { IndexPatternColumn, IndexPatternField, IndexPatternPrivateState, OperationType, BaseIndexPatternColumn, -} from '../indexpattern'; -import { termsOperation } from './terms'; -import { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; -import { dateHistogramOperation } from './date_histogram'; -import { countOperation } from './count'; +} from './indexpattern'; +import { termsOperation } from './operation_definitions/terms'; +import { + minOperation, + averageOperation, + sumOperation, + maxOperation, +} from './operation_definitions/metrics'; +import { dateHistogramOperation } from './operation_definitions/date_histogram'; +import { countOperation } from './operation_definitions/count'; type PossibleOperationDefinitions< U extends IndexPatternColumn = IndexPatternColumn From 41eb4d47aa2bb9aeb361f74a1d8e1b9000eb3e9d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 13 Jun 2019 13:21:43 +0200 Subject: [PATCH 16/27] fix tests for dimension panel --- .../dimension_panel/dimension_panel.test.tsx | 234 ++++++++++++++---- .../dimension_panel/field_select.tsx | 38 ++- .../dimension_panel/settings.tsx | 1 + 3 files changed, 206 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index 307faa2eb9c43..011e9e050364a 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -6,12 +6,13 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { EuiComboBox } from '@elastic/eui'; +import { EuiComboBox, EuiContextMenuItem } from '@elastic/eui'; import { IndexPatternPrivateState } from '../indexpattern'; import { getPotentialColumns, getColumnOrder } from '../operations'; import { IndexPatternDimensionPanel } from './dimension_panel'; import { DropHandler, DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; +import { act } from 'react-dom/test-utils'; jest.mock('../operations'); @@ -57,7 +58,7 @@ describe('IndexPatternDimensionPanel', () => { operationId: 'op1', label: 'Value of timestamp', dataType: 'date', - isBucketed: false, + isBucketed: true, // Private operationType: 'date_histogram', @@ -123,8 +124,27 @@ describe('IndexPatternDimensionPanel', () => { expect(filterOperations).toBeCalled(); }); + it('should show field select combo box on click', () => { + const wrapper = mount( + {}} + columnId={'col2'} + filterOperations={() => true} + /> + ); + + wrapper + .find('[data-test-subj="indexPattern-configure-dimension"]') + .first() + .simulate('click'); + + expect(wrapper.find(EuiComboBox).length).toEqual(1); + }); + it('should not show any choices if the filter returns false', () => { - const wrapper = shallow( + const wrapper = mount( { /> ); + wrapper + .find('[data-test-subj="indexPattern-configure-dimension"]') + .first() + .simulate('click'); + expect(wrapper.find(EuiComboBox)!.prop('options')!.length).toEqual(0); }); - it('should list all field names in sorted order', () => { - const wrapper = shallow( + it('should list all field names and document as a whole in sorted order', () => { + const wrapper = mount( { /> ); + wrapper + .find('[data-test-subj="indexPattern-configure-dimension"]') + .first() + .simulate('click'); + const options = wrapper.find(EuiComboBox).prop('options'); - expect(options!.map(({ label }) => label)).toEqual([ + expect(options![0].label).toEqual('Document'); + + expect(options![1].options!.map(({ label }) => label)).toEqual([ 'bytes', - 'documents', 'source', 'timestamp', ]); }); - it("should disable functions that won't work with the current column", () => { + it('should show all functions that work with the current column', () => { const setState = jest.fn(); - const wrapper = shallow( + const wrapper = mount( true} /> ); - expect( - wrapper.find('[data-test-subj="lns-indexPatternDimension-terms"]').prop('isDisabled') - ).toEqual(true); - expect( - wrapper.find('[data-test-subj="lns-indexPatternDimension-date_histogram"]').prop('isDisabled') - ).toEqual(false); - expect( - wrapper.find('[data-test-subj="lns-indexPatternDimension-sum"]').prop('isDisabled') - ).toEqual(true); - expect( - wrapper.find('[data-test-subj="lns-indexPatternDimension-avg"]').prop('isDisabled') - ).toEqual(true); - expect( - wrapper.find('[data-test-subj="lns-indexPatternDimension-count"]').prop('isDisabled') - ).toEqual(true); + wrapper + .find('[data-test-subj="indexPattern-dimensionPopover-button"]') + .first() + .simulate('click'); + + expect(wrapper.find(EuiContextMenuItem).map(instance => instance.text())).toEqual([ + 'Minimum', + 'Maximum', + 'Average', + 'Sum', + ]); }); it('should update the datasource state on selection of an operation', () => { const setState = jest.fn(); - const wrapper = shallow( + const wrapper = mount( true} suggestedPriority={1} /> ); - const comboBox = wrapper.find(EuiComboBox)!; - const firstOption = comboBox.prop('options')![0]; + wrapper + .find('[data-test-subj="indexPattern-dimensionPopover-button"]') + .first() + .simulate('click'); - comboBox.prop('onChange')!([firstOption]); + wrapper + .find('[data-test-subj="lns-indexPatternDimension-min"]') + .first() + .simulate('click'); expect(setState).toHaveBeenCalledWith({ ...state, columns: { ...state.columns, - col2: expect.objectContaining({ - sourceField: firstOption.label, + col1: expect.objectContaining({ + operationType: 'min', + sourceField: 'bytes', // Other parts of this don't matter for this test }), }, - columnOrder: ['col1', 'col2'], }); }); - it('should always request the new sort order when changing the function', () => { + it('should update the datasource state on selection of a field', () => { const setState = jest.fn(); - const wrapper = shallow( + const wrapper = mount( { /> ); - wrapper.find('[data-test-subj="lns-indexPatternDimension-date_histogram"]').simulate('click'); + wrapper + .find('[data-test-subj="indexPattern-configure-dimension"]') + .first() + .simulate('click'); - expect(getColumnOrder).toHaveBeenCalledWith({ - col1: expect.objectContaining({ - sourceField: 'timestamp', - operationType: 'date_histogram', - }), + const comboBox = wrapper.find(EuiComboBox)!; + const option = comboBox.prop('options')![1].options![1]; + + comboBox.prop('onChange')!([option]); + + expect(setState).toHaveBeenCalledWith({ + ...state, + columns: { + ...state.columns, + col1: expect.objectContaining({ + operationType: 'terms', + sourceField: 'source', + // Other parts of this don't matter for this test + }), + }, }); }); - it('should update the datasource state when the user makes a selection', () => { + it('should add a column on selection of a field', () => { const setState = jest.fn(); - const wrapper = shallow( + const wrapper = mount( op.dataType === 'number'} + filterOperations={() => true} + suggestedPriority={1} /> ); + wrapper + .find('[data-test-subj="indexPattern-configure-dimension"]') + .first() + .simulate('click'); + const comboBox = wrapper.find(EuiComboBox)!; - const firstField = comboBox.prop('options')![0]; + const option = comboBox.prop('options')![1].options![0]; - comboBox.prop('onChange')!([firstField]); + comboBox.prop('onChange')!([option]); expect(setState).toHaveBeenCalledWith({ ...state, columns: { ...state.columns, col2: expect.objectContaining({ - operationId: firstField.value, - label: 'Average of bytes', - dataType: 'number', - isBucketed: false, - operationType: 'avg', sourceField: 'bytes', + // Other parts of this don't matter for this test }), }, columnOrder: ['col1', 'col2'], }); }); + it('should always request the new sort order when changing the function', () => { + const setState = jest.fn(); + + const wrapper = mount( + true} + suggestedPriority={1} + /> + ); + + wrapper + .find('[data-test-subj="indexPattern-dimensionPopover-button"]') + .first() + .simulate('click'); + + wrapper + .find('[data-test-subj="lns-indexPatternDimension-min"]') + .first() + .simulate('click'); + + expect(getColumnOrder).toHaveBeenCalledWith({ + col1: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'min', + }), + }); + }); + it('should clear the dimension with the clear button', () => { const setState = jest.fn(); - const wrapper = shallow( + const wrapper = mount( { /> ); - const clearButton = wrapper.find('[data-test-subj="indexPattern-dimensionPopover-remove"]'); + const clearButton = wrapper.find( + 'EuiButtonIcon[data-test-subj="indexPattern-dimensionPopover-remove"]' + ); clearButton.simulate('click'); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx index 6b02775cc2118..c82c4e4b71f44 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx @@ -39,6 +39,30 @@ export function FieldSelect({ .concat(fieldColumns), col => col.sourceField ); + + uniqueColumnsByField.sort((column1, column2) => { + return column1.sourceField.localeCompare(column2.sourceField); + }); + + const fieldOptions = []; + const fieldLessColumn = filteredColumns.find(column => !('sourceField' in column)); + if (fieldLessColumn) { + fieldOptions.push({ + label: 'Document', + value: fieldLessColumn.operationId, + }); + } + + if (uniqueColumnsByField.length > 0) { + fieldOptions.push({ + label: 'Individual fields', + options: uniqueColumnsByField.map(col => ({ + label: col.sourceField, + value: col.operationId, + })), + }); + } + return ( <> @@ -66,19 +90,7 @@ export function FieldSelect({ }} data-test-subj="indexPattern-dimension-field" placeholder="Field" - options={[ - { - label: 'Document', - value: filteredColumns.find(column => !('sourceField' in column))!.operationId, - }, - { - label: 'Individual fields', - options: uniqueColumnsByField.map(col => ({ - label: col.sourceField, - value: col.operationId, - })), - }, - ]} + options={fieldOptions} selectedOptions={ selectedColumn && 'sourceField' in selectedColumn ? [ diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx index 752bf95a5c851..d56868da910b9 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx @@ -62,6 +62,7 @@ export function Settings({ .filter(o => selectedColumn && functionsFromField.some(col => col.operationType === o)) .map(o => ( { From 0674c2c007449fef33572f5b7027f09053151ed0 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 13 Jun 2019 15:33:07 +0200 Subject: [PATCH 17/27] fix tests --- .../lens/public/indexpattern_plugin/indexpattern.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index b1a539088d62b..26df8466fbb81 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -247,7 +247,7 @@ describe('IndexPattern Data Source', () => { index=\\"1\\" metricsAtAllLevels=\\"false\\" partialRows=\\"false\\" - aggConfigs='[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"timeRange\\":{\\"from\\":\\"now-1d\\",\\"to\\":\\"now\\"},\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1h\\",\\"drop_partials\\":false,\\"min_doc_count\\":1,\\"extended_bounds\\":{}}}]' | lens_rename_columns idMap='{\\"col-0-col1\\":\\"col1\\",\\"col-1-col2\\":\\"col2\\"}'" + aggConfigs='[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"timeRange\\":{\\"from\\":\\"now-1d\\",\\"to\\":\\"now\\"},\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":1,\\"extended_bounds\\":{}}}]' | lens_rename_columns idMap='{\\"col-0-col1\\":\\"col1\\",\\"col-1-col2\\":\\"col2\\"}'" `); }); }); From 6d420e6bad03e93b9ff033fd040995c0514b0592 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 13 Jun 2019 17:28:46 +0200 Subject: [PATCH 18/27] add tests and i18n --- .../dimension_panel/dimension_panel.test.tsx | 1 - .../dimension_panel/field_select.tsx | 16 +- .../dimension_panel/settings.tsx | 4 +- .../date_histogram.test.tsx | 92 +++++++++++ .../operation_definitions/terms.test.tsx | 154 ++++++++++++++++++ .../operation_definitions/utils.test.ts | 40 +++++ 6 files changed, 301 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.test.ts diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index 011e9e050364a..b0dc4822b1abb 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -12,7 +12,6 @@ import { getPotentialColumns, getColumnOrder } from '../operations'; import { IndexPatternDimensionPanel } from './dimension_panel'; import { DropHandler, DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; -import { act } from 'react-dom/test-utils'; jest.mock('../operations'); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx index c82c4e4b71f44..8c18a6ca0e419 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx @@ -48,14 +48,18 @@ export function FieldSelect({ const fieldLessColumn = filteredColumns.find(column => !('sourceField' in column)); if (fieldLessColumn) { fieldOptions.push({ - label: 'Document', + label: i18n.translate('xpack.lens.indexPattern.documentField', { + defaultMessage: 'Document', + }), value: fieldLessColumn.operationId, }); } if (uniqueColumnsByField.length > 0) { fieldOptions.push({ - label: 'Individual fields', + label: i18n.translate('xpack.lens.indexPattern.individualFieldsLabel', { + defaultMessage: 'Individual fields', + }), options: uniqueColumnsByField.map(col => ({ label: col.sourceField, value: col.operationId, @@ -89,7 +93,9 @@ export function FieldSelect({ setFieldSelectOpen(false); }} data-test-subj="indexPattern-dimension-field" - placeholder="Field" + placeholder={i18n.translate('xpack.lens.indexPattern.fieldPlaceholder', { + defaultMessage: 'Field', + })} options={fieldOptions} selectedOptions={ selectedColumn && 'sourceField' in selectedColumn @@ -129,7 +135,9 @@ export function FieldSelect({ iconType="cross" iconSize="s" color="danger" - aria-label="Remove" + aria-label={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', { + defaultMessage: 'Remove', + })} onClick={() => { const newColumns: IndexPatternPrivateState['columns'] = { ...state.columns, diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx index d56868da910b9..29c1b88bfc6f2 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx @@ -110,7 +110,9 @@ export function Settings({ setSettingsOpen(!isSettingsOpen); }} iconType="gear" - aria-label="Settings" + aria-label={i18n.translate('xpack.lens.indexPattern.settingsLabel', { + defaultMessage: 'Settings', + })} /> } diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx new file mode 100644 index 0000000000000..2b632dfa47d0a --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx @@ -0,0 +1,92 @@ +/* + * 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 React from 'react'; +import { dateHistogramOperation } from './date_histogram'; +import { shallow } from 'enzyme'; +import { DateHistogramIndexPatternColumn, IndexPatternPrivateState } from '../indexpattern'; +import { EuiRange } from '@elastic/eui'; + +describe('date_histogram', () => { + let state: IndexPatternPrivateState; + const InlineOptions = dateHistogramOperation.inlineOptions!; + + beforeEach(() => { + state = { + indexPatterns: {}, + currentIndexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'Value of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: 'w', + }, + sourceField: 'timestamp', + }, + }, + }; + }); + + describe('toEsAggsConfig', () => { + it('should reflect params correctly', () => { + const esAggsConfig = dateHistogramOperation.toEsAggsConfig( + state.columns.col1 as DateHistogramIndexPatternColumn, + 'col1' + ); + expect(esAggsConfig).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + interval: 'w', + field: 'timestamp', + }), + }) + ); + }); + }); + + describe('param editor', () => { + it('should render current value', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + + ); + + expect(instance.find(EuiRange).prop('value')).toEqual(1); + }); + + it('should update state with the interval value', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + + ); + + instance.find(EuiRange).prop('onChange')!({ + target: { + value: '2', + }, + } as React.ChangeEvent); + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + columns: { + ...state.columns, + col1: { + ...state.columns.col1, + params: { + interval: 'd', + }, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx new file mode 100644 index 0000000000000..656366dfb85fd --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx @@ -0,0 +1,154 @@ +/* + * 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 React from 'react'; +import { termsOperation } from './terms'; +import { shallow } from 'enzyme'; +import { IndexPatternPrivateState, TermsIndexPatternColumn } from '../indexpattern'; +import { EuiRange, EuiSelect } from '@elastic/eui'; + +describe('date_histogram', () => { + let state: IndexPatternPrivateState; + const InlineOptions = termsOperation.inlineOptions!; + const contextMenu = termsOperation.contextMenu!; + + beforeEach(() => { + state = { + indexPatterns: {}, + currentIndexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + operationId: 'op1', + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 5, + }, + sourceField: 'category', + }, + col2: { + operationId: 'op1', + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + }, + }, + }; + }); + + describe('toEsAggsConfig', () => { + it('should reflect params correctly', () => { + const esAggsConfig = termsOperation.toEsAggsConfig( + state.columns.col1 as TermsIndexPatternColumn, + 'col1' + ); + expect(esAggsConfig).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + orderBy: '_key', + field: 'category', + size: 5, + }), + }) + ); + }); + }); + + describe('popover param editor', () => { + it('should render current value and options', () => { + const setStateSpy = jest.fn(); + const PartialMenu = () => ( + <>{contextMenu({ state, setState: setStateSpy, columnId: 'col1' })}/> + ); + const instance = shallow(); + + expect(instance.find(EuiSelect).prop('value')).toEqual('alphabetical'); + expect( + instance + .find(EuiSelect) + .prop('options') + .map(({ value }) => value) + ).toEqual(['column$$$col2', 'alphabetical']); + }); + + it('should update state with the order value', () => { + const setStateSpy = jest.fn(); + const PartialMenu = () => ( + <>{contextMenu({ state, setState: setStateSpy, columnId: 'col1' })}/> + ); + const instance = shallow(); + + instance.find(EuiSelect).prop('onChange')!({ + target: { + value: 'column$$$col2', + }, + } as React.ChangeEvent); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + columns: { + ...state.columns, + col1: { + ...state.columns.col1, + params: { + ...(state.columns.col1 as TermsIndexPatternColumn).params, + orderBy: { + type: 'column', + columnId: 'col2', + }, + }, + }, + }, + }); + }); + }); + + describe('inline param editor', () => { + it('should render current value', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + + ); + + expect(instance.find(EuiRange).prop('value')).toEqual(5); + }); + + it('should update state with the size value', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + + ); + + instance.find(EuiRange).prop('onChange')!({ + target: { + value: '7', + }, + } as React.ChangeEvent); + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + columns: { + ...state.columns, + col1: { + ...state.columns.col1, + params: { + ...(state.columns.col1 as TermsIndexPatternColumn).params, + size: 7, + }, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.test.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.test.ts new file mode 100644 index 0000000000000..4cd47dc72b7ac --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { updateColumnParam } from './utils'; +import { IndexPatternPrivateState, DateHistogramIndexPatternColumn } from '../indexpattern'; + +describe('operation utils', () => { + it('should set the param for the given column', () => { + const currentColumn: DateHistogramIndexPatternColumn = { + operationId: 'op1', + label: 'Value of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + }; + + const state: IndexPatternPrivateState = { + indexPatterns: {}, + currentIndexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: currentColumn, + }, + }; + + expect(updateColumnParam(state, currentColumn, 'interval', 'M').columns.col1).toEqual({ + ...currentColumn, + params: { interval: 'M' }, + }); + }); +}); From 2f545589d529ee35947eb20433376050a89509d3 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 13 Jun 2019 17:51:47 +0200 Subject: [PATCH 19/27] added a few more tests --- .../__mocks__/operations.ts | 1 + .../dimension_panel/dimension_panel.test.tsx | 65 ++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts index e0ad308cb3032..dd3b3d9aaff77 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts @@ -8,6 +8,7 @@ const actual = jest.requireActual('../operations'); jest.spyOn(actual, 'getPotentialColumns'); jest.spyOn(actual, 'getColumnOrder'); +jest.spyOn(actual.operationDefinitionMap.date_histogram, 'inlineOptions'); export const { getPotentialColumns, diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index b0dc4822b1abb..86e23f6091e05 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -8,10 +8,11 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { EuiComboBox, EuiContextMenuItem } from '@elastic/eui'; import { IndexPatternPrivateState } from '../indexpattern'; -import { getPotentialColumns, getColumnOrder } from '../operations'; +import { getPotentialColumns, getColumnOrder, operationDefinitionMap } from '../operations'; import { IndexPatternDimensionPanel } from './dimension_panel'; import { DropHandler, DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; +import { act } from 'react-dom/test-utils'; jest.mock('../operations'); @@ -161,6 +162,68 @@ describe('IndexPatternDimensionPanel', () => { expect(wrapper.find(EuiComboBox)!.prop('options')!.length).toEqual(0); }); + it('should render the inline options directly', () => { + mount( + {}} + columnId={'col1'} + filterOperations={() => false} + /> + ); + + expect(operationDefinitionMap.date_histogram.inlineOptions as jest.Mock).toHaveBeenCalled(); + }); + + it('should not render the settings button if there are no settings or options', () => { + const wrapper = mount( + {}} + columnId={'col1'} + filterOperations={() => false} + /> + ); + + expect(wrapper.find('[data-test-subj="indexPattern-dimensionPopover-button"]').length).toBe(0); + }); + + it('should render the settings button if there are settings', () => { + const wrapper = mount( + {}} + columnId={'col1'} + filterOperations={() => false} + /> + ); + + expect( + wrapper.find('EuiButtonIcon[data-test-subj="indexPattern-dimensionPopover-button"]').length + ).toBe(1); + }); + it('should list all field names and document as a whole in sorted order', () => { const wrapper = mount( Date: Thu, 13 Jun 2019 17:52:52 +0200 Subject: [PATCH 20/27] add test subj --- .../indexpattern_plugin/operation_definitions/terms.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx index 39879c2f8ad11..98f9be51f3ef7 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx @@ -112,7 +112,10 @@ export const termsOperation: OperationDefinition = { text: 'Alphabetical', }); return [ - + Date: Thu, 13 Jun 2019 18:10:51 +0200 Subject: [PATCH 21/27] remove unused import --- .../indexpattern_plugin/dimension_panel/dimension_panel.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index 86e23f6091e05..5a5d546eb1b44 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -12,7 +12,6 @@ import { getPotentialColumns, getColumnOrder, operationDefinitionMap } from '../ import { IndexPatternDimensionPanel } from './dimension_panel'; import { DropHandler, DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; -import { act } from 'react-dom/test-utils'; jest.mock('../operations'); From 992da75f2c6899ffb76024f6bd88ea0b30bf3fa2 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 14 Jun 2019 11:08:35 +0200 Subject: [PATCH 22/27] add missing i18ns --- .../operation_definitions/date_histogram.tsx | 10 ++++++++-- .../operation_definitions/terms.tsx | 20 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx index c9aefe86d4448..32d84b22782b0 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx @@ -92,7 +92,11 @@ export const dateHistogramOperation: OperationDefinition - +
diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx index 98f9be51f3ef7..6251cf720250b 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx @@ -109,14 +109,20 @@ export const termsOperation: OperationDefinition = { }); orderOptions.push({ value: toValue({ type: 'alphabetical' }), - text: 'Alphabetical', + text: i18n.translate('xpack.lens.indexPattern.terms.orderAlphabetical', { + defaultMessage: 'Alphabetical', + }), }); return [ - + = { const currentColumn = state.columns[currentColumnId] as TermsIndexPatternColumn; return ( - + = { onChange={(e: React.ChangeEvent) => setState(updateColumnParam(state, currentColumn, 'size', Number(e.target.value))) } - aria-label="Number of values" + aria-label={i18n.translate('xpack.lens.indexPattern.terms.size', { + defaultMessage: 'Number of values', + })} /> From 087e43c9854e7ce52d7ca371939761c66b81482c Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 14 Jun 2019 11:53:32 +0200 Subject: [PATCH 23/27] refactor and add some more tests --- .../__mocks__/operations.ts | 1 - .../__mocks__/state_helpers.ts | 11 + .../dimension_panel/dimension_panel.test.tsx | 16 +- .../dimension_panel/dimension_panel.tsx | 14 +- .../dimension_panel/field_select.tsx | 29 +-- .../dimension_panel/settings.tsx | 19 +- .../operation_definitions/date_histogram.tsx | 2 +- .../operation_definitions/terms.tsx | 2 +- .../operation_definitions/utils.test.ts | 40 ---- .../operation_definitions/utils.ts | 43 ---- .../indexpattern_plugin/operations.test.ts | 123 +--------- .../public/indexpattern_plugin/operations.ts | 21 +- .../indexpattern_plugin/state_helpers.test.ts | 214 ++++++++++++++++++ .../indexpattern_plugin/state_helpers.ts | 91 ++++++++ .../indexpattern_plugin/to_expression.ts | 2 +- 15 files changed, 342 insertions(+), 286 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts delete mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.test.ts delete mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/state_helpers.ts diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts index dd3b3d9aaff77..9c07b07ae27fa 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts @@ -7,7 +7,6 @@ const actual = jest.requireActual('../operations'); jest.spyOn(actual, 'getPotentialColumns'); -jest.spyOn(actual, 'getColumnOrder'); jest.spyOn(actual.operationDefinitionMap.date_histogram, 'inlineOptions'); export const { diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts new file mode 100644 index 0000000000000..f80d4792f3494 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +const actual = jest.requireActual('../state_helpers'); + +jest.spyOn(actual, 'changeColumn'); + +export const { getColumnOrder, changeColumn, deleteColumn, updateColumnParam } = actual; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index 5a5d546eb1b44..aa119185eb226 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -8,11 +8,13 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { EuiComboBox, EuiContextMenuItem } from '@elastic/eui'; import { IndexPatternPrivateState } from '../indexpattern'; -import { getPotentialColumns, getColumnOrder, operationDefinitionMap } from '../operations'; +import { changeColumn } from '../state_helpers'; +import { getPotentialColumns, operationDefinitionMap } from '../operations'; import { IndexPatternDimensionPanel } from './dimension_panel'; import { DropHandler, DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; +jest.mock('../state_helpers'); jest.mock('../operations'); const expectedIndexPatterns = { @@ -417,7 +419,7 @@ describe('IndexPatternDimensionPanel', () => { }); }); - it('should always request the new sort order when changing the function', () => { + it('should use helper function when changing the function', () => { const setState = jest.fn(); const wrapper = mount( @@ -456,12 +458,14 @@ describe('IndexPatternDimensionPanel', () => { .first() .simulate('click'); - expect(getColumnOrder).toHaveBeenCalledWith({ - col1: expect.objectContaining({ + expect(changeColumn).toHaveBeenCalledWith( + expect.anything(), + 'col1', + expect.objectContaining({ sourceField: 'bytes', operationType: 'min', - }), - }); + }) + ); }); it('should clear the dimension with the clear button', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index 5c4824f9a6863..7b4a7c8ba21cd 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -15,10 +15,11 @@ import { IndexPatternField, } from '../indexpattern'; -import { getPotentialColumns, operationDefinitionMap, getColumnOrder } from '../operations'; +import { getPotentialColumns, operationDefinitionMap } from '../operations'; import { FieldSelect } from './field_select'; import { Settings } from './settings'; import { DragContextState, ChildDragDropProvider, DragDrop } from '../../drag_drop'; +import { changeColumn } from '../state_helpers'; export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { state: IndexPatternPrivateState; @@ -66,16 +67,7 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp return; } - const newColumns: IndexPatternPrivateState['columns'] = { - ...props.state.columns, - [props.columnId]: column, - }; - - props.setState({ - ...props.state, - columnOrder: getColumnOrder(newColumns), - columns: newColumns, - }); + props.setState(changeColumn(props.state, props.columnId, column)); }} > diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx index 8c18a6ca0e419..8e40565999a97 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx @@ -8,13 +8,9 @@ import _ from 'lodash'; import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox, EuiButtonEmpty, EuiButtonIcon, EuiFlexItem } from '@elastic/eui'; -import { - IndexPatternColumn, - FieldBasedIndexPatternColumn, - IndexPatternPrivateState, -} from '../indexpattern'; +import { IndexPatternColumn, FieldBasedIndexPatternColumn } from '../indexpattern'; import { IndexPatternDimensionPanelProps } from './dimension_panel'; -import { getColumnOrder } from '../operations'; +import { changeColumn, deleteColumn } from '../state_helpers'; export interface FieldSelectProps extends IndexPatternDimensionPanelProps { selectedColumn: IndexPatternColumn; @@ -113,17 +109,9 @@ export function FieldSelect({ const column: IndexPatternColumn = filteredColumns.find( ({ operationId }) => operationId === choices[0].value )!; - const newColumns: IndexPatternPrivateState['columns'] = { - ...state.columns, - [columnId]: column, - }; setFieldSelectOpen(false); - setState({ - ...state, - columns: newColumns, - columnOrder: getColumnOrder(newColumns), - }); + setState(changeColumn(state, columnId, column)); }} /> )} @@ -139,16 +127,7 @@ export function FieldSelect({ defaultMessage: 'Remove', })} onClick={() => { - const newColumns: IndexPatternPrivateState['columns'] = { - ...state.columns, - }; - delete newColumns[columnId]; - - setState({ - ...state, - columns: newColumns, - columnOrder: getColumnOrder(newColumns), - }); + setState(deleteColumn(state, columnId)); }} /> diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx index 29c1b88bfc6f2..927c42c84bd5e 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx @@ -16,12 +16,8 @@ import { } from '@elastic/eui'; import { IndexPatternColumn } from '../indexpattern'; import { IndexPatternDimensionPanelProps } from './dimension_panel'; -import { - getColumnOrder, - operationDefinitionMap, - getOperations, - getOperationDisplay, -} from '../operations'; +import { operationDefinitionMap, getOperations, getOperationDisplay } from '../operations'; +import { changeColumn } from '../state_helpers'; export interface SettingsProps extends IndexPatternDimensionPanelProps { selectedColumn: IndexPatternColumn; @@ -74,16 +70,7 @@ export function Settings({ col.sourceField === selectedColumn.sourceField) )!; - const newColumns = { - ...state.columns, - [columnId]: newColumn, - }; - - setState({ - ...state, - columnOrder: getColumnOrder(newColumns), - columns: newColumns, - }); + setState(changeColumn(state, columnId, newColumn)); }} > {operationPanels[o].displayName} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx index 32d84b22782b0..65edb8271c3bf 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx @@ -10,7 +10,7 @@ import { EuiForm, EuiFormRow, EuiRange } from '@elastic/eui'; import { IndexPatternField, DateHistogramIndexPatternColumn } from '../indexpattern'; import { DimensionPriority } from '../../types'; import { OperationDefinition } from '../operations'; -import { updateColumnParam } from './utils'; +import { updateColumnParam } from '../state_helpers'; type PropType = C extends React.ComponentType ? P : unknown; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx index 6251cf720250b..d8a98eba90f86 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx @@ -10,7 +10,7 @@ import { EuiForm, EuiFormRow, EuiRange, EuiSelect, EuiContextMenuItem } from '@e import { IndexPatternField, TermsIndexPatternColumn } from '../indexpattern'; import { DimensionPriority } from '../../types'; import { OperationDefinition } from '../operations'; -import { updateColumnParam } from './utils'; +import { updateColumnParam } from '../state_helpers'; type PropType = C extends React.ComponentType ? P : unknown; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.test.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.test.ts deleted file mode 100644 index 4cd47dc72b7ac..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { updateColumnParam } from './utils'; -import { IndexPatternPrivateState, DateHistogramIndexPatternColumn } from '../indexpattern'; - -describe('operation utils', () => { - it('should set the param for the given column', () => { - const currentColumn: DateHistogramIndexPatternColumn = { - operationId: 'op1', - label: 'Value of timestamp', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - params: { - interval: '1d', - }, - sourceField: 'timestamp', - }; - - const state: IndexPatternPrivateState = { - indexPatterns: {}, - currentIndexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: currentColumn, - }, - }; - - expect(updateColumnParam(state, currentColumn, 'interval', 'M').columns.col1).toEqual({ - ...currentColumn, - params: { interval: 'M' }, - }); - }); -}); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.ts deleted file mode 100644 index d0a6c08db9d18..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 { - IndexPatternPrivateState, - IndexPatternColumn, - BaseIndexPatternColumn, -} from '../indexpattern'; - -export function updateColumnParam< - C extends BaseIndexPatternColumn & { params: object }, - K extends keyof C['params'] ->( - state: IndexPatternPrivateState, - currentColumn: C, - paramName: K, - value: C['params'][K] -): IndexPatternPrivateState { - const columnId = Object.entries(state.columns).find( - ([_, column]) => column === currentColumn - )![0]; - - if (!('params' in state.columns[columnId])) { - throw new Error('Invariant: no params in this column'); - } - - return { - ...state, - columns: { - ...state.columns, - [columnId]: ({ - ...currentColumn, - params: { - ...currentColumn.params, - [paramName]: value, - }, - } as unknown) as IndexPatternColumn, - }, - }; -} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts index 7399a97d110cb..a24a97920bb3a 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOperationTypesForField, getPotentialColumns, getColumnOrder } from './operations'; +import { getOperationTypesForField, getPotentialColumns } from './operations'; import { IndexPatternPrivateState } from './indexpattern'; const expectedIndexPatterns = { @@ -213,124 +213,3 @@ Array [ }); }); }); - -describe('getColumnOrder', () => { - it('should work for empty columns', () => { - expect(getColumnOrder({})).toEqual([]); - }); - - it('should work for one column', () => { - expect( - getColumnOrder({ - col1: { - operationId: 'op1', - label: 'Value of timestamp', - dataType: 'string', - isBucketed: false, - - // Private - operationType: 'date_histogram', - sourceField: 'timestamp', - params: { - interval: 'h', - }, - }, - }) - ).toEqual(['col1']); - }); - - it('should put any number of aggregations before metrics', () => { - expect( - getColumnOrder({ - col1: { - operationId: 'op1', - label: 'Top Values of category', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - sourceField: 'category', - params: { - size: 5, - orderBy: { - type: 'alphabetical', - }, - }, - }, - col2: { - operationId: 'op2', - label: 'Average of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'avg', - sourceField: 'bytes', - }, - col3: { - operationId: 'op3', - label: 'Date Histogram of timestamp', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - sourceField: 'timestamp', - params: { - interval: '1d', - }, - }, - }) - ).toEqual(['col1', 'col3', 'col2']); - }); - - it('should reorder aggregations based on suggested priority', () => { - expect( - getColumnOrder({ - col1: { - operationId: 'op1', - label: 'Top Values of category', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - sourceField: 'category', - params: { - size: 5, - orderBy: { - type: 'alphabetical', - }, - }, - suggestedOrder: 2, - }, - col2: { - operationId: 'op2', - label: 'Average of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'avg', - sourceField: 'bytes', - suggestedOrder: 0, - }, - col3: { - operationId: 'op3', - label: 'Date Histogram of timestamp', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - sourceField: 'timestamp', - suggestedOrder: 1, - params: { - interval: '1d', - }, - }, - }) - ).toEqual(['col3', 'col1', 'col2']); - }); -}); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts index 59d4d795de969..40066bbf292be 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { DimensionPriority } from '../types'; import { IndexPatternColumn, @@ -35,6 +34,7 @@ type UnionToIntersection = (U extends U ? (k: U) => void : never) extends ((k ? I : never; +// this type makes sure that there is an operation definition for each column type export type AllOperationDefinitions = UnionToIntersection; export const operationDefinitionMap: AllOperationDefinitions = { @@ -57,7 +57,7 @@ export interface ParamEditorProps { setState: (newState: IndexPatternPrivateState) => void; columnId: string; } -export interface OperationDefinition { +export interface OperationDefinition { type: C['operationType']; displayName: string; // TODO make this a function dependend on the indexpattern with typeMeta information @@ -136,20 +136,3 @@ export function getPotentialColumns( return columns; } - -export function getColumnOrder(columns: Record): string[] { - const entries = Object.entries(columns); - - const [aggregations, metrics] = _.partition(entries, col => col[1].isBucketed); - - return aggregations - .sort(([id, col], [id2, col2]) => { - return ( - // Sort undefined orders last - (col.suggestedOrder !== undefined ? col.suggestedOrder : Number.MAX_SAFE_INTEGER) - - (col2.suggestedOrder !== undefined ? col2.suggestedOrder : Number.MAX_SAFE_INTEGER) - ); - }) - .map(([id]) => id) - .concat(metrics.map(([id]) => id)); -} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts new file mode 100644 index 0000000000000..88ba0b923d6ee --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts @@ -0,0 +1,214 @@ +/* + * 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 { updateColumnParam, getColumnOrder, changeColumn } from './state_helpers'; +import { IndexPatternPrivateState, DateHistogramIndexPatternColumn } from './indexpattern'; + +describe('state_helpers', () => { + describe('updateColumnParam', () => { + it('should set the param for the given column', () => { + const currentColumn: DateHistogramIndexPatternColumn = { + operationId: 'op1', + label: 'Value of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + }; + + const state: IndexPatternPrivateState = { + indexPatterns: {}, + currentIndexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: currentColumn, + }, + }; + + expect(updateColumnParam(state, currentColumn, 'interval', 'M').columns.col1).toEqual({ + ...currentColumn, + params: { interval: 'M' }, + }); + }); + }); + + describe('changeColumn', () => { + it('should update order on changing the column', () => { + const state: IndexPatternPrivateState = { + indexPatterns: {}, + currentIndexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + operationId: 'op1', + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'avg', + sourceField: 'bytes', + }, + col2: { + operationId: 'op1', + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'max', + sourceField: 'bytes', + }, + }, + }; + expect( + changeColumn(state, 'col2', { + operationId: 'op1', + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['col2', 'col1'], + }) + ); + }); + }); + + describe('getColumnOrder', () => { + it('should work for empty columns', () => { + expect(getColumnOrder({})).toEqual([]); + }); + + it('should work for one column', () => { + expect( + getColumnOrder({ + col1: { + operationId: 'op1', + label: 'Value of timestamp', + dataType: 'string', + isBucketed: false, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'h', + }, + }, + }) + ).toEqual(['col1']); + }); + + it('should put any number of aggregations before metrics', () => { + expect( + getColumnOrder({ + col1: { + operationId: 'op1', + label: 'Top Values of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + sourceField: 'category', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + }, + }, + col2: { + operationId: 'op2', + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'avg', + sourceField: 'bytes', + }, + col3: { + operationId: 'op3', + label: 'Date Histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: '1d', + }, + }, + }) + ).toEqual(['col1', 'col3', 'col2']); + }); + + it('should reorder aggregations based on suggested priority', () => { + expect( + getColumnOrder({ + col1: { + operationId: 'op1', + label: 'Top Values of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + sourceField: 'category', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + }, + suggestedOrder: 2, + }, + col2: { + operationId: 'op2', + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'avg', + sourceField: 'bytes', + suggestedOrder: 0, + }, + col3: { + operationId: 'op3', + label: 'Date Histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + suggestedOrder: 1, + params: { + interval: '1d', + }, + }, + }) + ).toEqual(['col3', 'col1', 'col2']); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_plugin/state_helpers.ts new file mode 100644 index 0000000000000..8c26662e6e149 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/state_helpers.ts @@ -0,0 +1,91 @@ +/* + * 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 _ from 'lodash'; +import { + IndexPatternPrivateState, + IndexPatternColumn, + BaseIndexPatternColumn, +} from './indexpattern'; + +export function getColumnOrder(columns: Record): string[] { + const entries = Object.entries(columns); + + const [aggregations, metrics] = _.partition(entries, col => col[1].isBucketed); + + return aggregations + .sort(([id, col], [id2, col2]) => { + return ( + // Sort undefined orders last + (col.suggestedOrder !== undefined ? col.suggestedOrder : Number.MAX_SAFE_INTEGER) - + (col2.suggestedOrder !== undefined ? col2.suggestedOrder : Number.MAX_SAFE_INTEGER) + ); + }) + .map(([id]) => id) + .concat(metrics.map(([id]) => id)); +} + +export function updateColumnParam< + C extends BaseIndexPatternColumn & { params: object }, + K extends keyof C['params'] +>( + state: IndexPatternPrivateState, + currentColumn: C, + paramName: K, + value: C['params'][K] +): IndexPatternPrivateState { + const columnId = Object.entries(state.columns).find( + ([_columnId, column]) => column === currentColumn + )![0]; + + if (!('params' in state.columns[columnId])) { + throw new Error('Invariant: no params in this column'); + } + + return { + ...state, + columns: { + ...state.columns, + [columnId]: ({ + ...currentColumn, + params: { + ...currentColumn.params, + [paramName]: value, + }, + } as unknown) as IndexPatternColumn, + }, + }; +} + +export function changeColumn( + state: IndexPatternPrivateState, + columnId: string, + newColumn: IndexPatternColumn +) { + const newColumns: IndexPatternPrivateState['columns'] = { + ...state.columns, + [columnId]: newColumn, + }; + + return { + ...state, + columnOrder: getColumnOrder(newColumns), + columns: newColumns, + }; +} + +export function deleteColumn(state: IndexPatternPrivateState, columnId: string) { + const newColumns: IndexPatternPrivateState['columns'] = { + ...state.columns, + }; + delete newColumns[columnId]; + + return { + ...state, + columns: newColumns, + columnOrder: getColumnOrder(newColumns), + }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts index 6f5b78a714063..c284f51bffe65 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -18,7 +18,7 @@ export function toExpression(state: IndexPatternPrivateState) { function getEsAggsConfig(column: C, columnId: string) { // Typescript is not smart enough to infer that definitionMap[C['operationType']] is always OperationDefinition, - // but this is made sure by the typing in operations/index.ts + // but this is made sure by the typing of the operation map const operationDefinition = (operationDefinitionMap[ column.operationType ] as unknown) as OperationDefinition; From b0a5ccfad95084f5aecb6d4937fe5db7453b0cad Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 14 Jun 2019 15:36:17 +0200 Subject: [PATCH 24/27] review comments --- .../editor_frame/suggestion_helpers.test.ts | 4 +-- .../editor_frame/workspace_panel.test.tsx | 22 ++++++------ .../__mocks__/state_helpers.ts | 9 ++++- .../dimension_panel/dimension_panel.test.tsx | 10 +++--- .../dimension_panel/dimension_panel.tsx | 18 ++++------ .../dimension_panel/field_select.tsx | 36 ++++++++++--------- .../dimension_panel/settings.tsx | 12 +++---- .../operation_definitions/terms.test.tsx | 2 +- .../indexpattern_plugin/operations.test.ts | 6 ++-- .../public/indexpattern_plugin/operations.ts | 10 ++---- .../indexpattern_plugin/state_helpers.ts | 14 ++++++++ .../xy_suggestions.test.ts | 6 ++-- 12 files changed, 81 insertions(+), 68 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts index b048b6e840484..850cdfc2b3c0f 100644 --- a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts @@ -30,7 +30,7 @@ describe('suggestion helpers', () => { 'vis1', {} ); - expect(suggestions.length).toBe(1); + expect(suggestions).toHaveLength(1); expect(suggestions[0].state).toBe(suggestedState); }); @@ -57,7 +57,7 @@ describe('suggestion helpers', () => { 'vis1', {} ); - expect(suggestions.length).toBe(3); + expect(suggestions).toHaveLength(3); }); it('should rank the visualizations by score', () => { diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index cfb51a0adce1d..5bdd100636766 100644 --- a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -56,8 +56,8 @@ describe('workspace_panel', () => { /> ); - expect(instance.find('[data-test-subj="empty-workspace"]').length).toBe(1); - expect(instance.find(expressionRendererMock).length).toBe(0); + expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(1); + expect(instance.find(expressionRendererMock)).toHaveLength(0); }); it('should render an explanatory text if the visualization does not produce an expression', () => { @@ -76,8 +76,8 @@ describe('workspace_panel', () => { /> ); - expect(instance.find('[data-test-subj="empty-workspace"]').length).toBe(1); - expect(instance.find(expressionRendererMock).length).toBe(0); + expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(1); + expect(instance.find(expressionRendererMock)).toHaveLength(0); }); it('should render an explanatory text if the datasource does not produce an expression', () => { @@ -96,8 +96,8 @@ describe('workspace_panel', () => { /> ); - expect(instance.find('[data-test-subj="empty-workspace"]').length).toBe(1); - expect(instance.find(expressionRendererMock).length).toBe(0); + expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(1); + expect(instance.find(expressionRendererMock)).toHaveLength(0); }); it('should render the resulting expression using the expression renderer', () => { @@ -158,8 +158,8 @@ Object { /> ); - expect(instance.find('[data-test-subj="expression-failure"]').length).toBe(1); - expect(instance.find(expressionRendererMock).length).toBe(0); + expect(instance.find('[data-test-subj="expression-failure"]')).toHaveLength(1); + expect(instance.find(expressionRendererMock)).toHaveLength(0); }); it('should show an error message if the expression fails to render', async () => { @@ -191,8 +191,8 @@ Object { instance.update(); - expect(instance.find('[data-test-subj="expression-failure"]').length).toBe(1); - expect(instance.find(expressionRendererMock).length).toBe(0); + expect(instance.find('[data-test-subj="expression-failure"]')).toHaveLength(1); + expect(instance.find(expressionRendererMock)).toHaveLength(0); }); it('should not attempt to run the expression again if it does not change', async () => { @@ -271,7 +271,7 @@ Object { expect(expressionRendererMock).toHaveBeenCalledTimes(2); - expect(instance.find(expressionRendererMock).length).toBe(1); + expect(instance.find(expressionRendererMock)).toHaveLength(1); }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts index f80d4792f3494..1df52a3fd80ea 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts @@ -8,4 +8,11 @@ const actual = jest.requireActual('../state_helpers'); jest.spyOn(actual, 'changeColumn'); -export const { getColumnOrder, changeColumn, deleteColumn, updateColumnParam } = actual; +export const { + getColumnOrder, + changeColumn, + deleteColumn, + updateColumnParam, + sortByField, + hasField, +} = actual; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index aa119185eb226..d3ed57b1eb941 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -141,7 +141,7 @@ describe('IndexPatternDimensionPanel', () => { .first() .simulate('click'); - expect(wrapper.find(EuiComboBox).length).toEqual(1); + expect(wrapper.find(EuiComboBox)).toHaveLength(1); }); it('should not show any choices if the filter returns false', () => { @@ -160,7 +160,7 @@ describe('IndexPatternDimensionPanel', () => { .first() .simulate('click'); - expect(wrapper.find(EuiComboBox)!.prop('options')!.length).toEqual(0); + expect(wrapper.find(EuiComboBox)!.prop('options')!).toHaveLength(0); }); it('should render the inline options directly', () => { @@ -174,7 +174,9 @@ describe('IndexPatternDimensionPanel', () => { /> ); - expect(operationDefinitionMap.date_histogram.inlineOptions as jest.Mock).toHaveBeenCalled(); + expect(operationDefinitionMap.date_histogram.inlineOptions as jest.Mock).toHaveBeenCalledTimes( + 1 + ); }); it('should not render the settings button if there are no settings or options', () => { @@ -188,7 +190,7 @@ describe('IndexPatternDimensionPanel', () => { /> ); - expect(wrapper.find('[data-test-subj="indexPattern-dimensionPopover-button"]').length).toBe(0); + expect(wrapper.find('[data-test-subj="indexPattern-dimensionPopover-button"]')).toHaveLength(0); }); it('should render the settings button if there are settings', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index 7b4a7c8ba21cd..bd318be148e17 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -19,7 +19,7 @@ import { getPotentialColumns, operationDefinitionMap } from '../operations'; import { FieldSelect } from './field_select'; import { Settings } from './settings'; import { DragContextState, ChildDragDropProvider, DragDrop } from '../../drag_drop'; -import { changeColumn } from '../state_helpers'; +import { changeColumn, hasField } from '../state_helpers'; export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { state: IndexPatternPrivateState; @@ -39,17 +39,15 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp const ParamEditor = selectedColumn && operationDefinitionMap[selectedColumn.operationType].inlineOptions; + function findColumnByField(field: IndexPatternField) { + return filteredColumns.find(col => hasField(col) && col.sourceField === field.name); + } + function canHandleDrop() { const { dragging } = props.dragDropContext; const field = dragging as IndexPatternField; - return ( - !!field && - !!field.type && - filteredColumns.some( - col => 'sourceField' in col && col.sourceField === (field as IndexPatternField).name - ) - ); + return !!field && !!field.type && !!findColumnByField(field as IndexPatternField); } return ( @@ -58,9 +56,7 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp data-test-subj="indexPattern-dropTarget" droppable={canHandleDrop()} onDrop={field => { - const column = columns.find( - col => 'sourceField' in col && col.sourceField === (field as IndexPatternField).name - ); + const column = findColumnByField(field as IndexPatternField); if (!column) { // TODO: What do we do if we couldn't find a column? diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx index 8e40565999a97..b14f9503dadf3 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiComboBox, EuiButtonEmpty, EuiButtonIcon, EuiFlexItem } from '@elastic/eui'; import { IndexPatternColumn, FieldBasedIndexPatternColumn } from '../indexpattern'; import { IndexPatternDimensionPanelProps } from './dimension_panel'; -import { changeColumn, deleteColumn } from '../state_helpers'; +import { changeColumn, deleteColumn, hasField, sortByField } from '../state_helpers'; export interface FieldSelectProps extends IndexPatternDimensionPanelProps { selectedColumn: IndexPatternColumn; @@ -25,23 +25,19 @@ export function FieldSelect({ setState, }: FieldSelectProps) { const [isFieldSelectOpen, setFieldSelectOpen] = useState(false); - const fieldColumns = filteredColumns.filter( - col => 'sourceField' in col - ) as FieldBasedIndexPatternColumn[]; + const fieldColumns = filteredColumns.filter(hasField) as FieldBasedIndexPatternColumn[]; - const uniqueColumnsByField = _.uniq( - fieldColumns - .filter(col => selectedColumn && col.operationType === selectedColumn.operationType) - .concat(fieldColumns), - col => col.sourceField + const uniqueColumnsByField = sortByField( + _.uniq( + fieldColumns + .filter(col => selectedColumn && col.operationType === selectedColumn.operationType) + .concat(fieldColumns), + col => col.sourceField + ) ); - uniqueColumnsByField.sort((column1, column2) => { - return column1.sourceField.localeCompare(column2.sourceField); - }); - const fieldOptions = []; - const fieldLessColumn = filteredColumns.find(column => !('sourceField' in column)); + const fieldLessColumn = filteredColumns.find(column => !hasField(column)); if (fieldLessColumn) { fieldOptions.push({ label: i18n.translate('xpack.lens.indexPattern.documentField', { @@ -94,7 +90,7 @@ export function FieldSelect({ })} options={fieldOptions} selectedOptions={ - selectedColumn && 'sourceField' in selectedColumn + selectedColumn && hasField(selectedColumn) ? [ { label: selectedColumn.sourceField, @@ -104,13 +100,19 @@ export function FieldSelect({ : [] } singleSelection={{ asPlainText: true }} - isClearable={false} + isClearable={true} onChange={choices => { + setFieldSelectOpen(false); + + if (choices.length === 0) { + setState(deleteColumn(state, columnId)); + return; + } + const column: IndexPatternColumn = filteredColumns.find( ({ operationId }) => operationId === choices[0].value )!; - setFieldSelectOpen(false); setState(changeColumn(state, columnId, column)); }} /> diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx index 927c42c84bd5e..b9f0cf771a34a 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx @@ -17,7 +17,7 @@ import { import { IndexPatternColumn } from '../indexpattern'; import { IndexPatternDimensionPanelProps } from './dimension_panel'; import { operationDefinitionMap, getOperations, getOperationDisplay } from '../operations'; -import { changeColumn } from '../state_helpers'; +import { changeColumn, hasField } from '../state_helpers'; export interface SettingsProps extends IndexPatternDimensionPanelProps { selectedColumn: IndexPatternColumn; @@ -46,9 +46,9 @@ export function Settings({ const functionsFromField = selectedColumn ? filteredColumns.filter(col => { return ( - (!('sourceField' in selectedColumn) && !('sourceField' in col)) || - ('sourceField' in selectedColumn && - 'sourceField' in col && + (!hasField(selectedColumn) && !hasField(col)) || + (hasField(selectedColumn) && + hasField(col) && col.sourceField === selectedColumn.sourceField) ); }) @@ -65,8 +65,8 @@ export function Settings({ const newColumn: IndexPatternColumn = filteredColumns.find( col => col.operationType === o && - (!('sourceField' in col) || - !('sourceField' in selectedColumn) || + (!hasField(col) || + !hasField(selectedColumn) || col.sourceField === selectedColumn.sourceField) )!; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx index 656366dfb85fd..4876ea42fc787 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx @@ -10,7 +10,7 @@ import { shallow } from 'enzyme'; import { IndexPatternPrivateState, TermsIndexPatternColumn } from '../indexpattern'; import { EuiRange, EuiSelect } from '@elastic/eui'; -describe('date_histogram', () => { +describe('terms', () => { let state: IndexPatternPrivateState; const InlineOptions = termsOperation.inlineOptions!; const contextMenu = termsOperation.contextMenu!; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts index a24a97920bb3a..e22a2317ef9c8 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts @@ -6,6 +6,7 @@ import { getOperationTypesForField, getPotentialColumns } from './operations'; import { IndexPatternPrivateState } from './indexpattern'; +import { hasField } from './state_helpers'; const expectedIndexPatterns = { 1: { @@ -174,10 +175,7 @@ describe('getOperationTypesForField', () => { const columns = getPotentialColumns(state); expect( - columns.map(col => [ - 'sourceField' in col ? col.sourceField : '_documents_', - col.operationType, - ]) + columns.map(col => [hasField(col) ? col.sourceField : '_documents_', col.operationType]) ).toMatchInlineSnapshot(` Array [ Array [ diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts index 40066bbf292be..778817071dcc0 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts @@ -21,6 +21,7 @@ import { } from './operation_definitions/metrics'; import { dateHistogramOperation } from './operation_definitions/date_histogram'; import { countOperation } from './operation_definitions/count'; +import { hasField, sortByField } from './state_helpers'; type PossibleOperationDefinitions< U extends IndexPatternColumn = IndexPatternColumn @@ -127,12 +128,5 @@ export function getPotentialColumns( } }); - columns.sort((column1, column2) => { - if ('sourceField' in column1 && 'sourceField' in column2) { - return column1.sourceField.localeCompare(column2.sourceField); - } - return column1.operationType.localeCompare(column2.operationType); - }); - - return columns; + return sortByField(columns); } diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_plugin/state_helpers.ts index 8c26662e6e149..aec5bf5a3b7b4 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/state_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/state_helpers.ts @@ -9,6 +9,7 @@ import { IndexPatternPrivateState, IndexPatternColumn, BaseIndexPatternColumn, + FieldBasedIndexPatternColumn, } from './indexpattern'; export function getColumnOrder(columns: Record): string[] { @@ -89,3 +90,16 @@ export function deleteColumn(state: IndexPatternPrivateState, columnId: string) columnOrder: getColumnOrder(newColumns), }; } + +export function hasField(column: BaseIndexPatternColumn): column is FieldBasedIndexPatternColumn { + return 'sourceField' in column; +} + +export function sortByField(columns: C[]) { + return [...columns].sort((column1, column2) => { + if (hasField(column1) && hasField(column2)) { + return column1.sourceField.localeCompare(column2.sourceField); + } + return column1.operationType.localeCompare(column2.operationType); + }); +} diff --git a/x-pack/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts index 83665dd8a6fe4..b034c9fe78b27 100644 --- a/x-pack/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts @@ -89,7 +89,7 @@ describe('xy_suggestions', () => { ], }); - expect(rest.length).toEqual(0); + expect(rest).toHaveLength(0); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` Object { "seriesType": "line", @@ -114,7 +114,7 @@ Object { ], }); - expect(rest.length).toEqual(0); + expect(rest).toHaveLength(0); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` Object { "seriesType": "line", @@ -147,7 +147,7 @@ Object { ], }); - expect(rest.length).toEqual(0); + expect(rest).toHaveLength(0); expect([suggestionSubset(s1), suggestionSubset(s2)]).toMatchInlineSnapshot(` Array [ Object { From 2fe0755fc873d07a4c25a98e14de95d861ed09d6 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 14 Jun 2019 15:36:57 +0200 Subject: [PATCH 25/27] remove unused import --- x-pack/plugins/lens/public/indexpattern_plugin/operations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts index 778817071dcc0..213055d40b7f4 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts @@ -21,7 +21,7 @@ import { } from './operation_definitions/metrics'; import { dateHistogramOperation } from './operation_definitions/date_histogram'; import { countOperation } from './operation_definitions/count'; -import { hasField, sortByField } from './state_helpers'; +import { sortByField } from './state_helpers'; type PossibleOperationDefinitions< U extends IndexPatternColumn = IndexPatternColumn From 77bb873c709661d94333d75ca9230705361633db Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 17 Jun 2019 11:09:31 +0200 Subject: [PATCH 26/27] start adding date histogram aggregation restrictions --- .../editor_frame_plugin/plugin.test.tsx | 2 +- .../indexpattern_plugin/indexpattern.tsx | 1 + .../lens/public/indexpattern_plugin/loader.ts | 2 +- .../operation_definitions/date_histogram.tsx | 89 +++++++++++++------ .../utils/parse_es_interval/index.ts | 9 ++ .../invalid_es_calendar_interval_error.ts | 39 ++++++++ .../invalid_es_interval_format_error.ts | 30 +++++++ .../parse_es_interval.test.ts | 56 ++++++++++++ .../parse_es_interval/parse_es_interval.ts | 62 +++++++++++++ 9 files changed, 263 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/index.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_calendar_interval_error.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_interval_format_error.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.test.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.ts diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/plugins/lens/public/editor_frame_plugin/plugin.test.tsx index 9875c7a8396d2..59c3c85f9dd28 100644 --- a/x-pack/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_plugin/plugin.test.tsx @@ -17,7 +17,7 @@ import { const waitForPromises = () => new Promise(resolve => setTimeout(resolve)); // mock away actual data plugin to prevent all of it being loaded -jest.mock('../../../../../src/legacy/core_plugins/data/public', () => {}); +jest.mock('../../../../../src/legacy/core_plugins/data/public/setup', () => {}); describe('editor_frame plugin', () => { let pluginInstance: EditorFramePlugin; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 6f81173403250..3ae7a20c24540 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -62,6 +62,7 @@ export interface DateHistogramIndexPatternColumn extends FieldBasedIndexPatternC operationType: 'date_histogram'; params: { interval: string; + timeZone?: string; }; } diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/loader.ts b/x-pack/plugins/lens/public/indexpattern_plugin/loader.ts index 41aa3737cde9b..8ad9e1beb8bf9 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_plugin/loader.ts @@ -24,8 +24,8 @@ interface SavedRestrictionsObject { string, { agg: string; - interval?: number; fixed_interval?: string; + calendar_interval?: string; delay?: string; time_zone?: string; } diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx index 65edb8271c3bf..b8483bf03adcd 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx @@ -11,6 +11,7 @@ import { IndexPatternField, DateHistogramIndexPatternColumn } from '../indexpatt import { DimensionPriority } from '../../types'; import { OperationDefinition } from '../operations'; import { updateColumnParam } from '../state_helpers'; +import { parseEsInterval } from './utils/parse_es_interval'; type PropType = C extends React.ComponentType ? P : unknown; @@ -47,16 +48,27 @@ export const dateHistogramOperation: OperationDefinition { const column = state.columns[columnId] as DateHistogramIndexPatternColumn; - const intervals = ['M', 'w', 'd', 'h']; + + const field = + column && + state.indexPatterns[state.currentIndexPatternId].fields.find( + currentField => currentField.name === column.sourceField + ); + const aggregationRestrictions = + field!.aggregationRestrictions && field!.aggregationRestrictions.date_histogram; + + const intervals = ['M', 'w', 'd', 'h'].filter(interval => { + if (!aggregationRestrictions) { + return true; + } + + if (!aggregationRestrictions.calendar_interval) { + return true; + } + + const restrictedInterval = parseEsInterval(aggregationRestrictions.calendar_interval); + return restrictedInterval.unit === interval && restrictedInterval.value === 1; + }); function intervalToNumeric(interval: string) { - return intervals.indexOf(interval); + const parsedInterval = parseEsInterval(interval); + return intervals.indexOf(parsedInterval.unit); } function numericToInterval(i: number) { return intervals[i]; } + return ( - ({ label: interval, value: index }))} - onChange={(e: React.ChangeEvent) => - setState( - updateColumnParam( - state, - column, - 'interval', - numericToInterval(Number(e.target.value)) + {intervals.length > 1 ? ( + ({ label: interval, value: index }))} + onChange={(e: React.ChangeEvent) => + setState( + updateColumnParam( + state, + column, + 'interval', + numericToInterval(Number(e.target.value)) + ) ) - ) - } - aria-label={i18n.translate('xpack.lens.indexPattern.dateHistogram.interval', { - defaultMessage: 'Level of detail', - })} - /> + } + aria-label={i18n.translate('xpack.lens.indexPattern.dateHistogram.interval', { + defaultMessage: 'Level of detail', + })} + /> + ) : ( + <>Interval fixed to {column.params.interval} due to aggregation restrictions. + )} ); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/index.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/index.ts new file mode 100644 index 0000000000000..88c5a9ce27ed2 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { parseEsInterval, ParsedInterval } from './parse_es_interval'; +export { InvalidEsCalendarIntervalError } from './invalid_es_calendar_interval_error'; +export { InvalidEsIntervalFormatError } from './invalid_es_interval_format_error'; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_calendar_interval_error.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_calendar_interval_error.ts new file mode 100644 index 0000000000000..e9e4471e90dc5 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_calendar_interval_error.ts @@ -0,0 +1,39 @@ +/* + * 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 { Unit } from '@elastic/datemath'; +import { i18n } from '@kbn/i18n'; + +export class InvalidEsCalendarIntervalError extends Error { + constructor( + public readonly interval: string, + public readonly value: number, + public readonly unit: Unit, + public readonly type: string + ) { + super( + i18n.translate('common.ui.parseEsInterval.invalidEsCalendarIntervalErrorMessage', { + defaultMessage: 'Invalid calendar interval: {interval}, value must be 1', + values: { interval }, + }) + ); + + this.name = 'InvalidEsCalendarIntervalError'; + this.value = value; + this.unit = unit; + this.type = type; + + // captureStackTrace is only available in the V8 engine, so any browser using + // a different JS engine won't have access to this method. + if (Error.captureStackTrace) { + Error.captureStackTrace(this, InvalidEsCalendarIntervalError); + } + + // Babel doesn't support traditional `extends` syntax for built-in classes. + // https://babeljs.io/docs/en/caveats/#classes + Object.setPrototypeOf(this, InvalidEsCalendarIntervalError.prototype); + } +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_interval_format_error.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_interval_format_error.ts new file mode 100644 index 0000000000000..17f558e01ceac --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_interval_format_error.ts @@ -0,0 +1,30 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export class InvalidEsIntervalFormatError extends Error { + constructor(public readonly interval: string) { + super( + i18n.translate('common.ui.parseEsInterval.invalidEsIntervalFormatErrorMessage', { + defaultMessage: 'Invalid interval format: {interval}', + values: { interval }, + }) + ); + + this.name = 'InvalidEsIntervalFormatError'; + + // captureStackTrace is only available in the V8 engine, so any browser using + // a different JS engine won't have access to this method. + if (Error.captureStackTrace) { + Error.captureStackTrace(this, InvalidEsIntervalFormatError); + } + + // Babel doesn't support traditional `extends` syntax for built-in classes. + // https://babeljs.io/docs/en/caveats/#classes + Object.setPrototypeOf(this, InvalidEsIntervalFormatError.prototype); + } +} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.test.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.test.ts new file mode 100644 index 0000000000000..76de807101559 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { InvalidEsCalendarIntervalError } from './invalid_es_calendar_interval_error'; +import { InvalidEsIntervalFormatError } from './invalid_es_interval_format_error'; +import { parseEsInterval } from './parse_es_interval'; + +describe('parseEsInterval', () => { + it('should correctly parse an interval containing unit and single value', () => { + expect(parseEsInterval('1ms')).toEqual({ value: 1, unit: 'ms', type: 'fixed' }); + expect(parseEsInterval('1s')).toEqual({ value: 1, unit: 's', type: 'fixed' }); + expect(parseEsInterval('1m')).toEqual({ value: 1, unit: 'm', type: 'calendar' }); + expect(parseEsInterval('1h')).toEqual({ value: 1, unit: 'h', type: 'calendar' }); + expect(parseEsInterval('1d')).toEqual({ value: 1, unit: 'd', type: 'calendar' }); + expect(parseEsInterval('1w')).toEqual({ value: 1, unit: 'w', type: 'calendar' }); + expect(parseEsInterval('1M')).toEqual({ value: 1, unit: 'M', type: 'calendar' }); + expect(parseEsInterval('1y')).toEqual({ value: 1, unit: 'y', type: 'calendar' }); + }); + + it('should correctly parse an interval containing unit and multiple value', () => { + expect(parseEsInterval('250ms')).toEqual({ value: 250, unit: 'ms', type: 'fixed' }); + expect(parseEsInterval('90s')).toEqual({ value: 90, unit: 's', type: 'fixed' }); + expect(parseEsInterval('60m')).toEqual({ value: 60, unit: 'm', type: 'fixed' }); + expect(parseEsInterval('12h')).toEqual({ value: 12, unit: 'h', type: 'fixed' }); + expect(parseEsInterval('7d')).toEqual({ value: 7, unit: 'd', type: 'fixed' }); + }); + + it('should throw a InvalidEsCalendarIntervalError for intervals containing calendar unit and multiple value', () => { + const intervals = ['4w', '12M', '10y']; + expect.assertions(intervals.length); + + intervals.forEach(interval => { + try { + parseEsInterval(interval); + } catch (error) { + expect(error instanceof InvalidEsCalendarIntervalError).toBe(true); + } + }); + }); + + it('should throw a InvalidEsIntervalFormatError for invalid interval formats', () => { + const intervals = ['1', 'h', '0m', '0.5h']; + expect.assertions(intervals.length); + + intervals.forEach(interval => { + try { + parseEsInterval(interval); + } catch (error) { + expect(error instanceof InvalidEsIntervalFormatError).toBe(true); + } + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.ts new file mode 100644 index 0000000000000..19ac03da03411 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.ts @@ -0,0 +1,62 @@ +/* + * 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 dateMath, { Unit } from '@elastic/datemath'; + +import { InvalidEsCalendarIntervalError } from './invalid_es_calendar_interval_error'; +import { InvalidEsIntervalFormatError } from './invalid_es_interval_format_error'; + +const ES_INTERVAL_STRING_REGEX = new RegExp( + '^([1-9][0-9]*)\\s*(' + dateMath.units.join('|') + ')$' +); + +export type ParsedInterval = ReturnType; + +/** + * Extracts interval properties from an ES interval string. Disallows unrecognized interval formats + * and fractional values. Converts some intervals from "calendar" to "fixed" when the number of + * units is larger than 1, and throws an error for others. + * + * Conversion rules: + * + * | Interval | Single unit type | Multiple units type | + * | -------- | ---------------- | ------------------- | + * | ms | fixed | fixed | + * | s | fixed | fixed | + * | m | calendar | fixed | + * | h | calendar | fixed | + * | d | calendar | fixed | + * | w | calendar | N/A - disallowed | + * | M | calendar | N/A - disallowed | + * | y | calendar | N/A - disallowed | + * + */ +export function parseEsInterval(interval: string) { + const matches = String(interval) + .trim() + .match(ES_INTERVAL_STRING_REGEX); + + if (!matches) { + throw new InvalidEsIntervalFormatError(interval); + } + + const value = parseFloat(matches[1]); + const unit = matches[2] as Unit; + const type = dateMath.unitsMap[unit].type; + + if (type === 'calendar' && value !== 1) { + throw new InvalidEsCalendarIntervalError(interval, value, unit, type); + } + + return { + value, + unit, + type: + (type === 'mixed' && value === 1) || type === 'calendar' + ? ('calendar' as 'calendar') + : ('fixed' as 'fixed'), + }; +} From 620e6372473d99c4138582ef1a9d3dc9a5cd9fb1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 17 Jun 2019 15:50:35 +0200 Subject: [PATCH 27/27] basic support for aggregation restrictions in date_histogram --- .../indexpattern_plugin/indexpattern.test.tsx | 4 +- .../date_histogram.test.tsx | 80 ++++++++++++++++++- .../operation_definitions/date_histogram.tsx | 32 +++----- .../utils/parse_es_interval/index.ts | 9 --- .../invalid_es_calendar_interval_error.ts | 39 --------- .../invalid_es_interval_format_error.ts | 30 ------- .../parse_es_interval.test.ts | 56 ------------- .../parse_es_interval/parse_es_interval.ts | 62 -------------- 8 files changed, 93 insertions(+), 219 deletions(-) delete mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/index.ts delete mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_calendar_interval_error.ts delete mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_interval_format_error.ts delete mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.test.ts delete mode 100644 x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.ts diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index 9efe38adbbbee..cb2c610aff548 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -283,7 +283,6 @@ describe('IndexPattern Data Source', () => { }), col2: expect.objectContaining({ operationType: 'count', - sourceField: 'documents', }), }, }) @@ -321,7 +320,6 @@ describe('IndexPattern Data Source', () => { }), col2: expect.objectContaining({ operationType: 'count', - sourceField: 'documents', }), }, }) @@ -359,7 +357,7 @@ describe('IndexPattern Data Source', () => { }), col2: expect.objectContaining({ sourceField: 'bytes', - operationType: 'sum', + operationType: 'min', }), }, }) diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx index 2b632dfa47d0a..94cd3e6495672 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx @@ -16,7 +16,21 @@ describe('date_histogram', () => { beforeEach(() => { state = { - indexPatterns: {}, + indexPatterns: { + 1: { + id: '1', + title: 'Mock Indexpattern', + fields: [ + { + name: 'timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + }, + ], + }, + }, currentIndexPatternId: '1', columnOrder: ['col1'], columns: { @@ -37,6 +51,38 @@ describe('date_histogram', () => { }; }); + describe('buildColumn', () => { + it('should create column object with default params', () => { + const column = dateHistogramOperation.buildColumn('op', 0, { + name: 'timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + }); + expect(column.params.interval).toEqual('h'); + }); + + it('should create column object with restrictions', () => { + const column = dateHistogramOperation.buildColumn('op', 0, { + name: 'timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + time_zone: 'UTC', + calendar_interval: '1y', + }, + }, + }); + expect(column.params.interval).toEqual('1y'); + expect(column.params.timeZone).toEqual('UTC'); + }); + }); + describe('toEsAggsConfig', () => { it('should reflect params correctly', () => { const esAggsConfig = dateHistogramOperation.toEsAggsConfig( @@ -88,5 +134,37 @@ describe('date_histogram', () => { }, }); }); + + it('should not render options if they are restricted', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + + ); + + expect(instance.find(EuiRange)).toHaveLength(0); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx index b8483bf03adcd..35a7de3ef6b6e 100644 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx @@ -6,12 +6,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiForm, EuiFormRow, EuiRange } from '@elastic/eui'; import { IndexPatternField, DateHistogramIndexPatternColumn } from '../indexpattern'; import { DimensionPriority } from '../../types'; import { OperationDefinition } from '../operations'; import { updateColumnParam } from '../state_helpers'; -import { parseEsInterval } from './utils/parse_es_interval'; type PropType = C extends React.ComponentType ? P : unknown; @@ -100,25 +100,13 @@ export const dateHistogramOperation: OperationDefinition currentField.name === column.sourceField ); - const aggregationRestrictions = + const intervalIsRestricted = field!.aggregationRestrictions && field!.aggregationRestrictions.date_histogram; - const intervals = ['M', 'w', 'd', 'h'].filter(interval => { - if (!aggregationRestrictions) { - return true; - } - - if (!aggregationRestrictions.calendar_interval) { - return true; - } - - const restrictedInterval = parseEsInterval(aggregationRestrictions.calendar_interval); - return restrictedInterval.unit === interval && restrictedInterval.value === 1; - }); + const intervals = ['M', 'w', 'd', 'h']; function intervalToNumeric(interval: string) { - const parsedInterval = parseEsInterval(interval); - return intervals.indexOf(parsedInterval.unit); + return intervals.indexOf(interval); } function numericToInterval(i: number) { @@ -132,7 +120,15 @@ export const dateHistogramOperation: OperationDefinition - {intervals.length > 1 ? ( + {intervalIsRestricted ? ( + + ) : ( - ) : ( - <>Interval fixed to {column.params.interval} due to aggregation restrictions. )} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/index.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/index.ts deleted file mode 100644 index 88c5a9ce27ed2..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * 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. - */ - -export { parseEsInterval, ParsedInterval } from './parse_es_interval'; -export { InvalidEsCalendarIntervalError } from './invalid_es_calendar_interval_error'; -export { InvalidEsIntervalFormatError } from './invalid_es_interval_format_error'; diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_calendar_interval_error.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_calendar_interval_error.ts deleted file mode 100644 index e9e4471e90dc5..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_calendar_interval_error.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 { Unit } from '@elastic/datemath'; -import { i18n } from '@kbn/i18n'; - -export class InvalidEsCalendarIntervalError extends Error { - constructor( - public readonly interval: string, - public readonly value: number, - public readonly unit: Unit, - public readonly type: string - ) { - super( - i18n.translate('common.ui.parseEsInterval.invalidEsCalendarIntervalErrorMessage', { - defaultMessage: 'Invalid calendar interval: {interval}, value must be 1', - values: { interval }, - }) - ); - - this.name = 'InvalidEsCalendarIntervalError'; - this.value = value; - this.unit = unit; - this.type = type; - - // captureStackTrace is only available in the V8 engine, so any browser using - // a different JS engine won't have access to this method. - if (Error.captureStackTrace) { - Error.captureStackTrace(this, InvalidEsCalendarIntervalError); - } - - // Babel doesn't support traditional `extends` syntax for built-in classes. - // https://babeljs.io/docs/en/caveats/#classes - Object.setPrototypeOf(this, InvalidEsCalendarIntervalError.prototype); - } -} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_interval_format_error.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_interval_format_error.ts deleted file mode 100644 index 17f558e01ceac..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/invalid_es_interval_format_error.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; - -export class InvalidEsIntervalFormatError extends Error { - constructor(public readonly interval: string) { - super( - i18n.translate('common.ui.parseEsInterval.invalidEsIntervalFormatErrorMessage', { - defaultMessage: 'Invalid interval format: {interval}', - values: { interval }, - }) - ); - - this.name = 'InvalidEsIntervalFormatError'; - - // captureStackTrace is only available in the V8 engine, so any browser using - // a different JS engine won't have access to this method. - if (Error.captureStackTrace) { - Error.captureStackTrace(this, InvalidEsIntervalFormatError); - } - - // Babel doesn't support traditional `extends` syntax for built-in classes. - // https://babeljs.io/docs/en/caveats/#classes - Object.setPrototypeOf(this, InvalidEsIntervalFormatError.prototype); - } -} diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.test.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.test.ts deleted file mode 100644 index 76de807101559..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 { InvalidEsCalendarIntervalError } from './invalid_es_calendar_interval_error'; -import { InvalidEsIntervalFormatError } from './invalid_es_interval_format_error'; -import { parseEsInterval } from './parse_es_interval'; - -describe('parseEsInterval', () => { - it('should correctly parse an interval containing unit and single value', () => { - expect(parseEsInterval('1ms')).toEqual({ value: 1, unit: 'ms', type: 'fixed' }); - expect(parseEsInterval('1s')).toEqual({ value: 1, unit: 's', type: 'fixed' }); - expect(parseEsInterval('1m')).toEqual({ value: 1, unit: 'm', type: 'calendar' }); - expect(parseEsInterval('1h')).toEqual({ value: 1, unit: 'h', type: 'calendar' }); - expect(parseEsInterval('1d')).toEqual({ value: 1, unit: 'd', type: 'calendar' }); - expect(parseEsInterval('1w')).toEqual({ value: 1, unit: 'w', type: 'calendar' }); - expect(parseEsInterval('1M')).toEqual({ value: 1, unit: 'M', type: 'calendar' }); - expect(parseEsInterval('1y')).toEqual({ value: 1, unit: 'y', type: 'calendar' }); - }); - - it('should correctly parse an interval containing unit and multiple value', () => { - expect(parseEsInterval('250ms')).toEqual({ value: 250, unit: 'ms', type: 'fixed' }); - expect(parseEsInterval('90s')).toEqual({ value: 90, unit: 's', type: 'fixed' }); - expect(parseEsInterval('60m')).toEqual({ value: 60, unit: 'm', type: 'fixed' }); - expect(parseEsInterval('12h')).toEqual({ value: 12, unit: 'h', type: 'fixed' }); - expect(parseEsInterval('7d')).toEqual({ value: 7, unit: 'd', type: 'fixed' }); - }); - - it('should throw a InvalidEsCalendarIntervalError for intervals containing calendar unit and multiple value', () => { - const intervals = ['4w', '12M', '10y']; - expect.assertions(intervals.length); - - intervals.forEach(interval => { - try { - parseEsInterval(interval); - } catch (error) { - expect(error instanceof InvalidEsCalendarIntervalError).toBe(true); - } - }); - }); - - it('should throw a InvalidEsIntervalFormatError for invalid interval formats', () => { - const intervals = ['1', 'h', '0m', '0.5h']; - expect.assertions(intervals.length); - - intervals.forEach(interval => { - try { - parseEsInterval(interval); - } catch (error) { - expect(error instanceof InvalidEsIntervalFormatError).toBe(true); - } - }); - }); -}); diff --git a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.ts b/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.ts deleted file mode 100644 index 19ac03da03411..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_plugin/operation_definitions/utils/parse_es_interval/parse_es_interval.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 dateMath, { Unit } from '@elastic/datemath'; - -import { InvalidEsCalendarIntervalError } from './invalid_es_calendar_interval_error'; -import { InvalidEsIntervalFormatError } from './invalid_es_interval_format_error'; - -const ES_INTERVAL_STRING_REGEX = new RegExp( - '^([1-9][0-9]*)\\s*(' + dateMath.units.join('|') + ')$' -); - -export type ParsedInterval = ReturnType; - -/** - * Extracts interval properties from an ES interval string. Disallows unrecognized interval formats - * and fractional values. Converts some intervals from "calendar" to "fixed" when the number of - * units is larger than 1, and throws an error for others. - * - * Conversion rules: - * - * | Interval | Single unit type | Multiple units type | - * | -------- | ---------------- | ------------------- | - * | ms | fixed | fixed | - * | s | fixed | fixed | - * | m | calendar | fixed | - * | h | calendar | fixed | - * | d | calendar | fixed | - * | w | calendar | N/A - disallowed | - * | M | calendar | N/A - disallowed | - * | y | calendar | N/A - disallowed | - * - */ -export function parseEsInterval(interval: string) { - const matches = String(interval) - .trim() - .match(ES_INTERVAL_STRING_REGEX); - - if (!matches) { - throw new InvalidEsIntervalFormatError(interval); - } - - const value = parseFloat(matches[1]); - const unit = matches[2] as Unit; - const type = dateMath.unitsMap[unit].type; - - if (type === 'calendar' && value !== 1) { - throw new InvalidEsCalendarIntervalError(interval, value, unit, type); - } - - return { - value, - unit, - type: - (type === 'mixed' && value === 1) || type === 'calendar' - ? ('calendar' as 'calendar') - : ('fixed' as 'fixed'), - }; -}