diff --git a/x-pack/plugins/lens/public/index.scss b/x-pack/plugins/lens/public/index.scss
index 6c01d745e0202..760223bd61642 100644
--- a/x-pack/plugins/lens/public/index.scss
+++ b/x-pack/plugins/lens/public/index.scss
@@ -1,5 +1,6 @@
// Import the EUI global scope so we can use EUI constants
@import 'src/legacy/ui/public/styles/_styling_constants';
-@import "./drag_drop/drag_drop.scss";
-@import "./xy_visualization_plugin/xy_expression.scss";
\ No newline at end of file
+@import './drag_drop/drag_drop.scss';
+@import './xy_visualization_plugin/xy_expression.scss';
+@import './indexpattern_plugin/indexpattern';
diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts
index ad0b78c63e710..532b6e66d2b27 100644
--- a/x-pack/plugins/lens/public/index.ts
+++ b/x-pack/plugins/lens/public/index.ts
@@ -7,6 +7,7 @@
export * from './types';
import 'ui/autoload/all';
+// Used to run esaggs queries
import 'uiExports/fieldFormats';
import 'uiExports/search';
import 'uiExports/visRequestHandlers';
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..7823768896d64 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-restricted-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/__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__/indexpattern.test.tsx.snap b/x-pack/plugins/lens/public/indexpattern_plugin/__snapshots__/indexpattern.test.tsx.snap
index 45760f0bf5efb..578eae643574f 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,40 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`IndexPattern Data Source #getPublicAPI renderDimensionPanel should render a dimension panel 1`] = `
-
- Dimension Panel
-
-
-`;
-
exports[`IndexPattern Data Source #renderDataPanel should match snapshot 1`] = `
Index Pattern Data Source
@@ -48,11 +13,11 @@ exports[`IndexPattern Data Source #renderDataPanel should match snapshot 1`] = `
options={
Array [
Object {
- "label": "Fake Index Pattern",
+ "label": "my-fake-index-pattern",
"value": "1",
},
Object {
- "label": "Fake Rollup Pattern",
+ "label": "my-fake-restricted-pattern",
"value": "2",
},
]
@@ -60,7 +25,7 @@ exports[`IndexPattern Data Source #renderDataPanel should match snapshot 1`] = `
selectedOptions={
Array [
Object {
- "label": "Fake Index Pattern",
+ "label": "my-fake-index-pattern",
"value": "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
new file mode 100644
index 0000000000000..2e1fb83a90571
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.test.tsx
@@ -0,0 +1,293 @@
+/*
+ * 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 { getColumnOrder, getPotentialColumns } from './operations';
+import { IndexPatternDimensionPanel } from './dimension_panel';
+
+jest.mock('./operations');
+
+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('IndexPatternDimensionPanel', () => {
+ 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',
+ },
+ },
+ };
+
+ jest.clearAllMocks();
+ });
+
+ 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 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);
+
+ 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 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(
+ 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-avg"]').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}
+ suggestedPriority={1}
+ />
+ );
+
+ const comboBox = wrapper.find(EuiComboBox)!;
+ const firstOption = comboBox.prop('options')![0];
+
+ comboBox.prop('onChange')!([firstOption]);
+
+ expect(setState).toHaveBeenCalledWith({
+ ...state,
+ columns: {
+ ...state.columns,
+ 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();
+
+ 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: expect.objectContaining({
+ operationId: firstField.value,
+ label: 'Value of bytes',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'value',
+ sourceField: 'bytes',
+ }),
+ },
+ 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
new file mode 100644
index 0000000000000..fd3600a3a616a
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_plugin/dimension_panel.tsx
@@ -0,0 +1,189 @@
+/*
+ * 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 {
+ EuiButtonIcon,
+ EuiComboBox,
+ EuiPopover,
+ EuiButtonEmpty,
+ EuiFlexItem,
+ EuiFlexGroup,
+} from '@elastic/eui';
+import { DatasourceDimensionPanelProps } from '../types';
+import { IndexPatternColumn, IndexPatternPrivateState, columnToOperation } from './indexpattern';
+
+import {
+ getOperationDisplay,
+ getOperations,
+ getPotentialColumns,
+ getColumnOrder,
+} from './operations';
+
+export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & {
+ state: IndexPatternPrivateState;
+ setState: (newState: IndexPatternPrivateState) => void;
+};
+
+export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) {
+ const [isOpen, setOpen] = 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 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.indexPattern.configureDimensionLabel', {
+ 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,
+ columnOrder: getColumnOrder(newColumns),
+ });
+ }}
+ />
+
+
+
+ {operations.map(o => (
+ col.operationType === o)}
+ onClick={() => {
+ if (!selectedColumn) {
+ return;
+ }
+
+ const newColumn: IndexPatternColumn = filteredColumns.find(
+ col =>
+ col.operationType === o && col.sourceField === selectedColumn.sourceField
+ )!;
+
+ const newColumns = {
+ ...props.state.columns,
+ [props.columnId]: newColumn,
+ };
+
+ props.setState({
+ ...props.state,
+ columnOrder: getColumnOrder(newColumns),
+ columns: newColumns,
+ });
+ }}
+ >
+ {operationPanels[o].displayName}
+
+ ))}
+
+
+
+
+
+ {selectedColumn && (
+
+ {
+ const newColumns: IndexPatternPrivateState['columns'] = {
+ ...props.state.columns,
+ };
+ delete newColumns[props.columnId];
+
+ props.setState({
+ ...props.state,
+ columns: newColumns,
+ 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
new file mode 100644
index 0000000000000..ac1b7d4ab754b
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.scss
@@ -0,0 +1,3 @@
+.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 ac0cc03fbd3ab..f5fa6da12eb3c 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';
@@ -21,7 +20,7 @@ jest.mock('./loader');
const expectedIndexPatterns = {
1: {
id: '1',
- title: 'Fake Index Pattern',
+ title: 'my-fake-index-pattern',
timeFieldName: 'timestamp',
fields: [
{
@@ -46,7 +45,7 @@ const expectedIndexPatterns = {
},
2: {
id: '2',
- title: 'Fake Rollup Pattern',
+ title: 'my-fake-restricted-pattern',
timeFieldName: 'timestamp',
fields: [
{
@@ -54,18 +53,50 @@ const expectedIndexPatterns = {
type: 'date',
aggregatable: true,
searchable: true,
+ aggregationRestrictions: {
+ date_histogram: {
+ agg: 'date_histogram',
+ fixed_interval: '1d',
+ delay: '7d',
+ time_zone: 'UTC',
+ },
+ },
},
{
name: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
+ aggregationRestrictions: {
+ // Ignored in the UI
+ histogram: {
+ agg: 'histogram',
+ interval: 1000,
+ },
+ avg: {
+ agg: 'avg',
+ },
+ max: {
+ agg: 'max',
+ },
+ min: {
+ agg: 'min',
+ },
+ sum: {
+ agg: 'sum',
+ },
+ },
},
{
name: 'source',
type: 'string',
aggregatable: true,
searchable: true,
+ aggregationRestrictions: {
+ terms: {
+ agg: 'terms',
+ },
+ },
},
],
},
@@ -178,7 +209,7 @@ describe('IndexPattern Data Source', () => {
// Private
operationType: 'value',
- sourceField: 'op',
+ sourceField: 'source',
},
col2: {
operationId: 'op2',
@@ -188,15 +219,52 @@ 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=\\"my-fake-index-pattern\\" 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\\"
+ 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\\":{}}}]'"
+`);
+ });
});
describe('#getPublicAPI', () => {
@@ -227,88 +295,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).toBeCalledTimes(3);
- });
-
- 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 22614914d4dda..940f89ae1d660 100644
--- a/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_plugin/indexpattern.tsx
@@ -4,19 +4,34 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import _ from 'lodash';
import React 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 { Datasource, DataType } from '..';
import uuid from 'uuid';
-import { DatasourceDimensionPanelProps, DatasourceDataPanelProps } from '../types';
+import { Datasource, DataType } from '..';
+import {
+ DatasourceDimensionPanelProps,
+ DatasourceDataPanelProps,
+ DimensionPriority,
+} from '../types';
import { getIndexPatterns } from './loader';
-
-type OperationType = 'value' | 'terms' | 'date_histogram';
-
-interface IndexPatternColumn {
+import { toExpression } from './to_expression';
+import { IndexPatternDimensionPanel } from './dimension_panel';
+
+export type OperationType =
+ | 'value'
+ | 'terms'
+ | 'date_histogram'
+ | 'sum'
+ | 'avg'
+ | 'min'
+ | 'max'
+ | 'count';
+
+export interface IndexPatternColumn {
// Public
operationId: string;
label: string;
@@ -26,13 +41,14 @@ interface IndexPatternColumn {
// Private
operationType: OperationType;
sourceField: string;
+ suggestedOrder?: DimensionPriority;
}
export interface IndexPattern {
id: string;
fields: IndexPatternField[];
title: string;
- timeFieldName?: string;
+ timeFieldName?: string | null;
}
export interface IndexPatternField {
@@ -41,6 +57,19 @@ export interface IndexPatternField {
esTypes?: string[];
aggregatable: boolean;
searchable: boolean;
+ aggregationRestrictions?: Partial<
+ Record<
+ string,
+ {
+ agg: string;
+ interval?: number;
+ fixed_interval?: string;
+ calendar_interval?: string;
+ delay?: string;
+ time_zone?: string;
+ }
+ >
+ >;
}
export interface IndexPatternPersistedState {
@@ -95,76 +124,50 @@ export function IndexPatternDataPanel(props: DatasourceDataPanelProps void;
-};
-
-export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) {
- 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,
- isBucketed: false,
-
- operationType: 'value' as OperationType,
- sourceField: field.name,
- }));
-
- const filteredColumns = columns.filter(col => {
- const { operationId, label, dataType, isBucketed } = col;
+export function columnToOperation(column: IndexPatternColumn) {
+ const { dataType, label, isBucketed, operationId } = column;
+ return {
+ id: operationId,
+ label,
+ dataType,
+ isBucketed,
+ };
+}
- return props.filterOperations({
- id: operationId,
- label,
- dataType,
- isBucketed,
+type UnwrapPromise = T extends Promise ? P : T;
+type InferFromArray = T extends Array ? P : T;
+
+function addRestrictionsToFields(
+ indexPattern: InferFromArray>, void>>
+): IndexPattern {
+ const { typeMeta } = indexPattern;
+ if (!typeMeta) {
+ return indexPattern;
+ }
+
+ const aggs = Object.keys(typeMeta.aggs);
+
+ const newFields = [...(indexPattern.fields as IndexPatternField[])];
+ newFields.forEach((field, index) => {
+ const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {};
+ aggs.forEach(agg => {
+ if (typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]) {
+ restrictionsObj[agg] = typeMeta.aggs[agg][field.name];
+ }
});
+ if (Object.keys(restrictionsObj).length) {
+ newFields[index] = { ...field, aggregationRestrictions: restrictionsObj };
+ }
});
- const selectedColumn: IndexPatternColumn | null = props.state.columns[props.columnId] || null;
-
- 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,
- };
+ const { id, title, timeFieldName } = indexPattern;
- props.setState({
- ...props.state,
- columns: newColumns,
- // Order is not meaningful until we aggregate
- columnOrder: Object.keys(newColumns),
- });
- }}
- />
-
- );
+ return {
+ id,
+ title,
+ timeFieldName: timeFieldName || undefined,
+ fields: newFields,
+ };
}
export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: ToastNotifications) {
@@ -176,7 +179,7 @@ export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: To
if (indexPatternObjects) {
indexPatternObjects.forEach(obj => {
- indexPatterns[obj.id] = obj;
+ indexPatterns[obj.id] = addRestrictionsToFields(obj);
});
}
@@ -198,18 +201,7 @@ export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: To
return { currentIndexPatternId, columns, columnOrder };
},
- toExpression(state: IndexPatternPrivateState) {
- if (state.columnOrder.length === 0) {
- return null;
- }
-
- const fieldNames = state.columnOrder.map(col => state.columns[col].sourceField);
- const expression = `esdocs index="${state.currentIndexPatternId}" fields="${fieldNames.join(
- ', '
- )}" sort="${fieldNames[0]}, DESC"`;
-
- return expression;
- },
+ toExpression,
renderDataPanel(
domElement: Element,
@@ -224,14 +216,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]);
},
generateColumnId: () => {
// TODO: Come up with a more compact form of generating unique column ids
@@ -240,7 +225,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..41aa3737cde9b 100644
--- a/x-pack/plugins/lens/public/indexpattern_plugin/loader.ts
+++ b/x-pack/plugins/lens/public/indexpattern_plugin/loader.ts
@@ -9,30 +9,54 @@ 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 SavedRestrictionsObject {
+ aggs: Record<
+ string,
+ Record<
+ string,
+ {
+ agg: string;
+ interval?: number;
+ fixed_interval?: string;
+ delay?: string;
+ time_zone?: string;
+ }
+ >
+ >;
+}
+type SavedRestrictionsInfo = SavedRestrictionsObject | 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;
- return Object.assign(attributes, {
+ const { id, attributes, type } = savedObject;
+ return {
+ ...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 SavedRestrictionsInfo)
+ : undefined,
+ };
});
})
.catch(err => {
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..9bc64b8b24e38
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.test.ts
@@ -0,0 +1,319 @@
+/*
+ * 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, 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', () => {
+ 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 restrictions', () => {
+ it('should return operations on strings', () => {
+ expect(
+ getOperationTypesForField({
+ type: 'string',
+ name: 'a',
+ aggregatable: true,
+ searchable: true,
+ aggregationRestrictions: {
+ terms: {
+ agg: 'terms',
+ },
+ },
+ })
+ ).toEqual(expect.arrayContaining(['terms']));
+ });
+
+ it('should return operations on numbers', () => {
+ expect(
+ getOperationTypesForField({
+ type: 'number',
+ name: 'a',
+ aggregatable: true,
+ searchable: true,
+ aggregationRestrictions: {
+ 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,
+ aggregationRestrictions: {
+ date_histogram: {
+ agg: 'date_histogram',
+ fixed_interval: '60m',
+ delay: '1d',
+ time_zone: 'UTC',
+ },
+ },
+ })
+ ).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
new file mode 100644
index 0000000000000..4a0df7a6a977a
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_plugin/operations.ts
@@ -0,0 +1,221 @@
+/*
+ * 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 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({ 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';
+ }
+}
+
+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
new file mode 100644
index 0000000000000..9e9f113665fdb
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_plugin/to_expression.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 null;
+ }
+
+ 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="${indexName}" fields="${fieldNames.join(', ')}" sort="${
+ 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 {
+ 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: '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='${JSON.stringify(aggs)}'`;
+ }
+
+ return '';
+}