From 4e361a6de1f38b2cbd1024181bf4b52884193863 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 15 Jul 2019 18:40:03 +0200 Subject: [PATCH] [Lens] Terms order direction (#39884) --- .../__mocks__/operations.ts | 1 + .../dimension_panel/dimension_panel.tsx | 3 +- .../dimension_panel/field_select.tsx | 2 +- .../dimension_panel/popover_editor.tsx | 3 +- .../indexpattern_plugin/indexpattern.test.tsx | 1 + .../indexpattern_plugin/indexpattern.tsx | 34 ++- .../operation_definitions/count.tsx | 6 +- .../date_histogram.test.tsx | 4 +- .../operation_definitions/date_histogram.tsx | 1 + .../filter_ratio.test.tsx | 2 +- .../operation_definitions/filter_ratio.tsx | 3 +- .../operation_definitions/metrics.tsx | 1 + .../operation_definitions/terms.test.tsx | 223 +++++++++++++++++- .../operation_definitions/terms.tsx | 72 +++++- .../indexpattern_plugin/operations.test.ts | 2 +- .../public/indexpattern_plugin/operations.ts | 14 +- .../indexpattern_plugin/state_helpers.test.ts | 152 +++++++++++- .../indexpattern_plugin/state_helpers.ts | 81 ++++--- .../indexpattern_plugin/to_expression.ts | 7 +- .../lens/public/indexpattern_plugin/utils.ts | 21 ++ 20 files changed, 561 insertions(+), 72 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts index 0602feff52d95e..81a77344f3993a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts @@ -8,6 +8,7 @@ const actual = jest.requireActual('../operations'); jest.spyOn(actual, 'getPotentialColumns'); jest.spyOn(actual.operationDefinitionMap.date_histogram, 'paramEditor'); +jest.spyOn(actual.operationDefinitionMap.terms, 'onOtherColumnChanged'); export const { getPotentialColumns, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index 55c7cddd7f5271..719d4fc5a89af0 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -21,7 +21,8 @@ import { import { getPotentialColumns } from '../operations'; import { PopoverEditor } from './popover_editor'; import { DragContextState, ChildDragDropProvider, DragDrop } from '../../drag_drop'; -import { changeColumn, hasField, deleteColumn } from '../state_helpers'; +import { changeColumn, deleteColumn } from '../state_helpers'; +import { hasField } from '../utils'; export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { state: IndexPatternPrivateState; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx index 215419cd3e9993..6fb1b8eb9f21af 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx @@ -15,7 +15,7 @@ import { OperationType, BaseIndexPatternColumn, } from '../indexpattern'; -import { hasField, sortByField } from '../state_helpers'; +import { hasField, sortByField } from '../utils'; export interface FieldSelectProps { incompatibleSelectedOperationType: OperationType | null; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx index 053c8e08349b19..560007d44862e2 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx @@ -22,8 +22,9 @@ import classNames from 'classnames'; import { IndexPatternColumn, OperationType } from '../indexpattern'; import { IndexPatternDimensionPanelProps } from './dimension_panel'; import { operationDefinitionMap, getOperationDisplay } from '../operations'; -import { hasField, deleteColumn, changeColumn } from '../state_helpers'; +import { deleteColumn, changeColumn } from '../state_helpers'; import { FieldSelect } from './field_select'; +import { hasField } from '../utils'; const operationPanels = getOperationDisplay(); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index db4bced265b0ca..b8d19fc1987bb7 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -145,6 +145,7 @@ describe('IndexPattern Data Source', () => { params: { size: 5, orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', }, }, }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 7328684d09777a..01d6ffd6018de9 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -73,6 +73,7 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { params: { size: number; orderBy: { type: 'alphabetical' } | { type: 'column'; columnId: string }; + orderDirection: 'asc' | 'desc'; }; } @@ -325,9 +326,18 @@ export function getIndexPatternDatasource({ const hasBucket = operations.find(op => op === 'date_histogram' || op === 'terms'); if (hasBucket) { - const column = buildColumnForOperationType(0, hasBucket, undefined, field); - - const countColumn = buildColumnForOperationType(1, 'count'); + const countColumn = buildColumnForOperationType(1, 'count', state.columns); + + // let column know about count column + const column = buildColumnForOperationType( + 0, + hasBucket, + { + col2: countColumn, + }, + undefined, + field + ); const suggestion: DatasourceSuggestion = { state: { @@ -362,9 +372,21 @@ export function getIndexPatternDatasource({ f => f.name === currentIndexPattern.timeFieldName )!; - const column = buildColumnForOperationType(0, operations[0], undefined, field); - - const dateColumn = buildColumnForOperationType(1, 'date_histogram', undefined, dateField); + const column = buildColumnForOperationType( + 0, + operations[0], + state.columns, + undefined, + field + ); + + const dateColumn = buildColumnForOperationType( + 1, + 'date_histogram', + state.columns, + undefined, + dateField + ); const suggestion: DatasourceSuggestion = { state: { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx index d37504ad32fe52..fd3b0aa06339e7 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx @@ -18,7 +18,11 @@ export const countOperation: OperationDefinition = { isApplicableForField: ({ aggregationRestrictions, type }) => { return false; }, - buildColumn(operationId: string, suggestedOrder?: DimensionPriority): CountIndexPatternColumn { + buildColumn( + operationId: string, + columns: {}, + suggestedOrder?: DimensionPriority + ): CountIndexPatternColumn { return { operationId, label: i18n.translate('xpack.lens.indexPattern.countOf', { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx index 59b463a545651f..e823040fee752f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx @@ -53,7 +53,7 @@ describe('date_histogram', () => { describe('buildColumn', () => { it('should create column object with default params', () => { - const column = dateHistogramOperation.buildColumn('op', 0, { + const column = dateHistogramOperation.buildColumn('op', {}, 0, { name: 'timestamp', type: 'date', esTypes: ['date'], @@ -64,7 +64,7 @@ describe('date_histogram', () => { }); it('should create column object with restrictions', () => { - const column = dateHistogramOperation.buildColumn('op', 0, { + const column = dateHistogramOperation.buildColumn('op', {}, 0, { name: 'timestamp', type: 'date', esTypes: ['date'], diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx index 753a950f07cdd2..43b03b330bd633 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx @@ -45,6 +45,7 @@ export const dateHistogramOperation: OperationDefinition { describe('buildColumn', () => { it('should create column object with default params', () => { - const column = filterRatioOperation.buildColumn('op', 0); + const column = filterRatioOperation.buildColumn('op', {}, 0); expect(column.params.numerator).toEqual({ query: '', language: 'kuery' }); expect(column.params.denominator).toEqual({ query: '', language: 'kuery' }); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx index 56a5cd77c5afd2..7e413d05f95173 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiFormRow } from '@elastic/eui'; import { Query } from '../../../../../../../src/legacy/core_plugins/data/public/query'; -import { FilterRatioIndexPatternColumn } from '../indexpattern'; +import { FilterRatioIndexPatternColumn, IndexPatternColumn } from '../indexpattern'; import { DimensionPriority } from '../../types'; import { OperationDefinition } from '../operations'; import { updateColumnParam } from '../state_helpers'; @@ -23,6 +23,7 @@ export const filterRatioOperation: OperationDefinition false, buildColumn( operationId: string, + _columns: Partial>, suggestedOrder?: DimensionPriority ): FilterRatioIndexPatternColumn { return { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx index cb460f8ede090f..41d5471e307f13 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx @@ -32,6 +32,7 @@ function buildMetricOperation( }, buildColumn( operationId: string, + columns: {}, suggestedOrder?: DimensionPriority, field?: IndexPatternField ): T { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx index 92b91975807756..9c69b4fd4e20ec 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx @@ -31,6 +31,7 @@ describe('terms', () => { params: { orderBy: { type: 'alphabetical' }, size: 5, + orderDirection: 'asc', }, sourceField: 'category', }, @@ -65,29 +66,185 @@ describe('terms', () => { }); }); + describe('buildColumn', () => { + it('should use existing metric column as order column', () => { + const termsColumn = termsOperation.buildColumn('abc', { + col1: { + operationId: 'op1', + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + }, + }); + expect(termsColumn.params).toEqual( + expect.objectContaining({ + orderBy: { type: 'column', columnId: 'col1' }, + }) + ); + }); + }); + + describe('onOtherColumnChanged', () => { + it('should keep the column if order by column still exists and is metric', () => { + const initialColumn: TermsIndexPatternColumn = { + operationId: 'op1', + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col1' }, + size: 5, + orderDirection: 'asc', + }, + sourceField: 'category', + }; + const updatedColumn = termsOperation.onOtherColumnChanged!(initialColumn, { + col1: { + operationId: 'op1', + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + }, + }); + expect(updatedColumn).toBe(initialColumn); + }); + + it('should switch to alphabetical ordering if the order column is removed', () => { + const termsColumn = termsOperation.onOtherColumnChanged!( + { + operationId: 'op1', + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col1' }, + size: 5, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + {} + ); + expect(termsColumn.params).toEqual( + expect.objectContaining({ + orderBy: { type: 'alphabetical' }, + }) + ); + }); + + it('should switch to alphabetical ordering if the order column is not a metric anymore', () => { + const termsColumn = termsOperation.onOtherColumnChanged!( + { + operationId: 'op1', + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col1' }, + size: 5, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + { + col1: { + operationId: 'op1', + label: 'Value of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: 'w', + }, + sourceField: 'timestamp', + }, + } + ); + expect(termsColumn.params).toEqual( + expect.objectContaining({ + orderBy: { type: 'alphabetical' }, + }) + ); + }); + }); + describe('popover param editor', () => { - it('should render current value and options', () => { + it('should render current order by value and options', () => { const setStateSpy = jest.fn(); const instance = shallow( ); - expect(instance.find(EuiSelect).prop('value')).toEqual('alphabetical'); - expect( - instance - .find(EuiSelect) - .prop('options') - .map(({ value }) => value) - ).toEqual(['column$$$col2', 'alphabetical']); + const select = instance.find('[data-test-subj="indexPattern-terms-orderBy"]').find(EuiSelect); + + expect(select.prop('value')).toEqual('alphabetical'); + + expect(select.prop('options').map(({ value }) => value)).toEqual([ + 'column$$$col2', + 'alphabetical', + ]); + }); + + it('should not show filter ratio column as sort target', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + + ); + + const select = instance.find('[data-test-subj="indexPattern-terms-orderBy"]').find(EuiSelect); + + expect(select.prop('options').map(({ value }) => value)).toEqual(['alphabetical']); }); - it('should update state with the order value', () => { + it('should update state with the order by value', () => { const setStateSpy = jest.fn(); const instance = shallow( ); - instance.find(EuiSelect).prop('onChange')!({ + instance + .find(EuiSelect) + .find('[data-test-subj="indexPattern-terms-orderBy"]') + .prop('onChange')!({ target: { value: 'column$$$col2', }, @@ -111,7 +268,51 @@ describe('terms', () => { }); }); - it('should render current value', () => { + it('should render current order direction value and options', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + + ); + + const select = instance + .find('[data-test-subj="indexPattern-terms-orderDirection"]') + .find(EuiSelect); + + expect(select.prop('value')).toEqual('asc'); + expect(select.prop('options').map(({ value }) => value)).toEqual(['asc', 'desc']); + }); + + it('should update state with the order direction value', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + + ); + + instance + .find('[data-test-subj="indexPattern-terms-orderDirection"]') + .find(EuiSelect) + .prop('onChange')!({ + target: { + value: 'desc', + }, + } as React.ChangeEvent); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + columns: { + ...state.columns, + col1: { + ...state.columns.col1, + params: { + ...(state.columns.col1 as TermsIndexPatternColumn).params, + orderDirection: 'desc', + }, + }, + }, + }); + }); + + it('should render current size value', () => { const setStateSpy = jest.fn(); const instance = shallow( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx index 98e3443549cedf..e36456ebe5dd36 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiForm, EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; -import { IndexPatternField, TermsIndexPatternColumn } from '../indexpattern'; +import { IndexPatternField, TermsIndexPatternColumn, IndexPatternColumn } from '../indexpattern'; import { DimensionPriority } from '../../types'; import { OperationDefinition } from '../operations'; import { updateColumnParam } from '../state_helpers'; @@ -31,6 +31,10 @@ function ofName(name: string) { }); } +function isSortableByColumn(column: IndexPatternColumn) { + return !column.isBucketed && column.operationType !== 'filter_ratio'; +} + export const termsOperation: OperationDefinition = { type: 'terms', displayName: i18n.translate('xpack.lens.indexPattern.terms', { @@ -44,9 +48,14 @@ export const termsOperation: OperationDefinition = { }, buildColumn( operationId: string, + columns: Partial>, suggestedOrder?: DimensionPriority, field?: IndexPatternField ): TermsIndexPatternColumn { + const existingMetricColumn = Object.entries(columns) + .filter(([_columnId, column]) => column && isSortableByColumn(column)) + .map(([id]) => id)[0]; + return { operationId, label: ofName(field ? field.name : ''), @@ -57,7 +66,10 @@ export const termsOperation: OperationDefinition = { isBucketed: true, params: { size: 5, - orderBy: { type: 'alphabetical' }, + orderBy: existingMetricColumn + ? { type: 'column', columnId: existingMetricColumn } + : { type: 'alphabetical' }, + orderDirection: existingMetricColumn ? 'desc' : 'asc', }, }; }, @@ -70,7 +82,7 @@ export const termsOperation: OperationDefinition = { field: column.sourceField, orderBy: column.params.orderBy.type === 'alphabetical' ? '_key' : column.params.orderBy.columnId, - order: 'desc', + order: column.params.orderDirection, size: column.params.size, otherBucket: false, otherBucketLabel: 'Other', @@ -78,6 +90,23 @@ export const termsOperation: OperationDefinition = { missingBucketLabel: 'Missing', }, }), + onOtherColumnChanged: (currentColumn, columns) => { + if (currentColumn.params.orderBy.type === 'column') { + // check whether the column is still there and still a metric + const columnSortedBy = columns[currentColumn.params.orderBy.columnId]; + if (!columnSortedBy || !isSortableByColumn(columnSortedBy)) { + return { + ...currentColumn, + params: { + ...currentColumn.params, + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + }, + }; + } + } + return currentColumn; + }, paramEditor: ({ state, setState, columnId: currentColumnId }) => { const currentColumn = state.columns[currentColumnId] as TermsIndexPatternColumn; const SEPARATOR = '$$$'; @@ -100,7 +129,7 @@ export const termsOperation: OperationDefinition = { } const orderOptions = Object.entries(state.columns) - .filter(([_columnId, column]) => !column.isBucketed) + .filter(([_columnId, column]) => isSortableByColumn(column)) .map(([columnId, column]) => { return { value: toValue({ type: 'column', columnId }), @@ -140,6 +169,7 @@ export const termsOperation: OperationDefinition = { })} > ) => @@ -152,6 +182,40 @@ export const termsOperation: OperationDefinition = { })} /> + + ) => + setState( + updateColumnParam(state, currentColumn, 'orderDirection', e.target.value as + | 'asc' + | 'desc') + ) + } + aria-label={i18n.translate('xpack.lens.indexPattern.terms.orderBy', { + defaultMessage: 'Order by', + })} + /> + ); }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.test.ts index f016515db32ea0..a835fa76ade8b7 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.test.ts @@ -6,7 +6,7 @@ import { getOperationTypesForField, getPotentialColumns } from './operations'; import { IndexPatternPrivateState } from './indexpattern'; -import { hasField } from './state_helpers'; +import { hasField } from './utils'; jest.mock('./loader'); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts index 8471e2b0ad8f41..0d9912e60d7b59 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts @@ -24,7 +24,7 @@ import { import { dateHistogramOperation } from './operation_definitions/date_histogram'; import { countOperation } from './operation_definitions/count'; import { filterRatioOperation } from './operation_definitions/filter_ratio'; -import { sortByField } from './state_helpers'; +import { sortByField } from './utils'; type PossibleOperationDefinitions< U extends IndexPatternColumn = IndexPatternColumn @@ -73,9 +73,14 @@ export interface OperationDefinition { isApplicableForField: (field: IndexPatternField) => boolean; buildColumn: ( operationId: string, + columns: Partial>, suggestedOrder?: DimensionPriority, field?: IndexPatternField ) => C; + onOtherColumnChanged?: ( + currentColumn: C, + columns: Partial> + ) => C; paramEditor?: React.ComponentType; toEsAggsConfig: (column: C, columnId: string) => unknown; } @@ -106,10 +111,11 @@ export function getOperationTypesForField(field: IndexPatternField): OperationTy export function buildColumnForOperationType( index: number, op: T, + columns: Partial>, suggestedOrder?: DimensionPriority, field?: IndexPatternField ): IndexPatternColumn { - return operationDefinitionMap[op].buildColumn(`${index}${op}`, suggestedOrder, field); + return operationDefinitionMap[op].buildColumn(`${index}${op}`, columns, suggestedOrder, field); } export function getPotentialColumns( @@ -123,14 +129,14 @@ export function getPotentialColumns( const validOperations = getOperationTypesForField(field); return validOperations.map(op => - buildColumnForOperationType(index, op, suggestedOrder, field) + buildColumnForOperationType(index, op, state.columns, suggestedOrder, field) ); }) .reduce((prev, current) => prev.concat(current)); operationDefinitions.forEach(operation => { if (operation.isApplicableWithoutField) { - columns.push(operation.buildColumn(operation.type, suggestedOrder)); + columns.push(operation.buildColumn(operation.type, state.columns, suggestedOrder)); } }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts index c021119323b116..2f4684d5c617d7 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts @@ -4,10 +4,102 @@ * 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'; +import { updateColumnParam, changeColumn, getColumnOrder, deleteColumn } from './state_helpers'; +import { + IndexPatternPrivateState, + DateHistogramIndexPatternColumn, + TermsIndexPatternColumn, + AvgIndexPatternColumn, +} from './indexpattern'; +import { operationDefinitionMap } from './operations'; + +jest.mock('./operations'); describe('state_helpers', () => { + describe('deleteColumn', () => { + it('should remove column', () => { + const termsColumn: TermsIndexPatternColumn = { + operationId: 'op2', + label: 'Top values of source', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + sourceField: 'source', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 5, + }, + }; + + const state: IndexPatternPrivateState = { + indexPatterns: {}, + currentIndexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: termsColumn, + col2: { + operationId: 'op1', + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + }, + }, + }; + + expect(deleteColumn(state, 'col2').columns).toEqual({ + col1: termsColumn, + }); + }); + + it('should execute adjustments for other columns', () => { + const termsColumn: TermsIndexPatternColumn = { + operationId: 'op2', + label: 'Top values of source', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + sourceField: 'source', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 5, + }, + }; + + const state: IndexPatternPrivateState = { + indexPatterns: {}, + currentIndexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: termsColumn, + col2: { + operationId: 'op1', + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + }, + }, + }; + + deleteColumn(state, 'col2'); + + expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith(termsColumn, { + col1: termsColumn, + }); + }); + }); + describe('updateColumnParam', () => { it('should set the param for the given column', () => { const currentColumn: DateHistogramIndexPatternColumn = { @@ -131,6 +223,60 @@ describe('state_helpers', () => { }) ); }); + + it('should execute adjustments for other columns', () => { + const termsColumn: TermsIndexPatternColumn = { + operationId: 'op2', + label: 'Top values of source', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + sourceField: 'source', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 5, + }, + }; + + const newColumn: AvgIndexPatternColumn = { + operationId: 'op1', + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'avg', + sourceField: 'bytes', + }; + + const state: IndexPatternPrivateState = { + indexPatterns: {}, + currentIndexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: termsColumn, + col2: { + operationId: 'op1', + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + }, + }, + }; + + changeColumn(state, 'col2', newColumn); + + expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith(termsColumn, { + col1: termsColumn, + col2: newColumn, + }); + }); }); describe('getColumnOrder', () => { @@ -175,6 +321,7 @@ describe('state_helpers', () => { orderBy: { type: 'alphabetical', }, + orderDirection: 'asc', }, }, col2: { @@ -221,6 +368,7 @@ describe('state_helpers', () => { orderBy: { type: 'alphabetical', }, + orderDirection: 'asc', }, suggestedOrder: 2, }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts index 0ee3222c1e5d75..394700d4469b68 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts @@ -9,25 +9,8 @@ import { IndexPatternPrivateState, IndexPatternColumn, BaseIndexPatternColumn, - FieldBasedIndexPatternColumn, } 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)); -} +import { operationDefinitionMap, OperationDefinition } from './operations'; export function updateColumnParam< C extends BaseIndexPatternColumn & { params: object }, @@ -61,6 +44,25 @@ export function updateColumnParam< }; } +function adjustColumnReferencesForChangedColumn( + columns: IndexPatternPrivateState['columns'], + columnId: string +) { + const newColumns = { ...columns }; + Object.keys(newColumns).forEach(currentColumnId => { + if (currentColumnId !== columnId) { + const currentColumn = newColumns[currentColumnId] as BaseIndexPatternColumn; + const operationDefinition = operationDefinitionMap[ + currentColumn.operationType + ] as OperationDefinition; + newColumns[currentColumnId] = (operationDefinition.onOtherColumnChanged + ? operationDefinition.onOtherColumnChanged(currentColumn, newColumns) + : currentColumn) as IndexPatternColumn; + } + }); + return newColumns; +} + export function changeColumn( state: IndexPatternPrivateState, columnId: string, @@ -77,10 +79,13 @@ export function changeColumn( ? ({ ...newColumn, params: oldColumn.params } as IndexPatternColumn) : newColumn; - const newColumns: IndexPatternPrivateState['columns'] = { - ...state.columns, - [columnId]: updatedColumn, - }; + const newColumns: IndexPatternPrivateState['columns'] = adjustColumnReferencesForChangedColumn( + { + ...state.columns, + [columnId]: updatedColumn, + }, + columnId + ); return { ...state, @@ -90,27 +95,33 @@ export function changeColumn( } export function deleteColumn(state: IndexPatternPrivateState, columnId: string) { - const newColumns: IndexPatternPrivateState['columns'] = { + const columns: IndexPatternPrivateState['columns'] = { ...state.columns, }; - delete newColumns[columnId]; + delete columns[columnId]; + + const newColumns = adjustColumnReferencesForChangedColumn(columns, columnId); return { ...state, - columns: newColumns, columnOrder: getColumnOrder(newColumns), + columns: newColumns, }; } -export function hasField(column: BaseIndexPatternColumn): column is FieldBasedIndexPatternColumn { - return 'sourceField' in column; -} +export function getColumnOrder(columns: Record): string[] { + const entries = Object.entries(columns); -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); - }); + 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/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts index 67887ef186f097..8ed92c2b5fca05 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -51,7 +51,12 @@ export function toExpression(state: IndexPatternPrivateState) { ); if (filterRatios.length) { - const countColumn = buildColumnForOperationType(columnEntries.length, 'count', 2); + const countColumn = buildColumnForOperationType( + columnEntries.length, + 'count', + state.columns, + 2 + ); aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); return `esaggs diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts new file mode 100644 index 00000000000000..33b0efdf350c05 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts @@ -0,0 +1,21 @@ +/* + * 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 { BaseIndexPatternColumn, FieldBasedIndexPatternColumn } from './indexpattern'; + +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); + }); +}