diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.test.tsx new file mode 100644 index 0000000000000..c9d7ce1ea81f8 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.test.tsx @@ -0,0 +1,237 @@ +/* + * 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 } from 'enzyme'; +import React from 'react'; +import { BucketNestingEditor } from './bucket_nesting_editor'; +import { IndexPatternColumn } from '../indexpattern'; + +describe('BucketNestingEditor', () => { + function mockCol(col: Partial = {}): IndexPatternColumn { + const result = { + dataType: 'string', + isBucketed: true, + label: 'a', + operationType: 'terms', + params: { + size: 5, + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + }, + sourceField: 'a', + suggestedPriority: 0, + ...col, + }; + + return result as IndexPatternColumn; + } + + it('should display an unchecked switch if there are two buckets and it is the root', () => { + const component = mount( + + ); + const control = component.find('[data-test-subj="indexPattern-nesting-switch"]').first(); + + expect(control.prop('checked')).toBeFalsy(); + }); + + it('should display a checked switch if there are two buckets and it is not the root', () => { + const component = mount( + + ); + const control = component.find('[data-test-subj="indexPattern-nesting-switch"]').first(); + + expect(control.prop('checked')).toBeTruthy(); + }); + + it('should reorder the columns when toggled', () => { + const setColumns = jest.fn(); + const component = mount( + + ); + const control = component.find('[data-test-subj="indexPattern-nesting-switch"]').first(); + + (control.prop('onChange') as () => {})(); + + expect(setColumns).toHaveBeenCalledWith(['a', 'b', 'c']); + }); + + it('should display nothing if there are no buckets', () => { + const component = mount( + + ); + + expect(component.children().length).toBe(0); + }); + + it('should display nothing if there is one bucket', () => { + const component = mount( + + ); + + expect(component.children().length).toBe(0); + }); + + it('should display a dropdown with the parent column selected if 3+ buckets', () => { + const component = mount( + + ); + + const control = component.find('[data-test-subj="indexPattern-nesting-select"]').first(); + + expect(control.prop('value')).toEqual('c'); + }); + + it('should reorder the columns when a column is selected in the dropdown', () => { + const setColumns = jest.fn(); + const component = mount( + + ); + + const control = component.find('[data-test-subj="indexPattern-nesting-select"]').first(); + (control.prop('onChange') as (e: unknown) => {})({ + target: { value: 'b' }, + }); + + expect(setColumns).toHaveBeenCalledWith(['c', 'b', 'a']); + }); + + it('should move to root if the first dropdown item is selected', () => { + const setColumns = jest.fn(); + const component = mount( + + ); + + const control = component.find('[data-test-subj="indexPattern-nesting-select"]').first(); + (control.prop('onChange') as (e: unknown) => {})({ + target: { value: '' }, + }); + + expect(setColumns).toHaveBeenCalledWith(['a', 'c', 'b']); + }); + + it('should allow the last bucket to be moved', () => { + const setColumns = jest.fn(); + const component = mount( + + ); + + const control = component.find('[data-test-subj="indexPattern-nesting-select"]').first(); + (control.prop('onChange') as (e: unknown) => {})({ + target: { value: '' }, + }); + + expect(setColumns).toHaveBeenCalledWith(['b', 'c', 'a']); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx new file mode 100644 index 0000000000000..630dc6252b6ee --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx @@ -0,0 +1,96 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiHorizontalRule, EuiSwitch, EuiSelect, EuiFormLabel } from '@elastic/eui'; +import { IndexPatternLayer } from '../indexpattern'; + +function nestColumn(columnOrder: string[], outer: string, inner: string) { + const result = columnOrder.filter(c => c !== inner); + const outerPosition = result.indexOf(outer); + + result.splice(outerPosition + 1, 0, inner); + + return result; +} + +export function BucketNestingEditor({ + columnId, + layer, + setColumns, +}: { + columnId: string; + layer: IndexPatternLayer; + setColumns: (columns: string[]) => void; +}) { + const column = layer.columns[columnId]; + const columns = Object.entries(layer.columns); + const aggColumns = columns + .filter(([id, c]) => id !== columnId && c.isBucketed) + .map(([value, c]) => ({ value, text: c.label })); + + if (!column || !column.isBucketed || !aggColumns.length) { + return null; + } + + const prevColumn = layer.columnOrder[layer.columnOrder.indexOf(columnId) - 1]; + + if (aggColumns.length === 1) { + const [target] = aggColumns; + + return ( + + <> + + { + if (prevColumn) { + setColumns(nestColumn(layer.columnOrder, columnId, target.value)); + } else { + setColumns(nestColumn(layer.columnOrder, target.value, columnId)); + } + }} + /> + + + ); + } + + return ( + + <> + + + {i18n.translate('xpack.lens.xyChart.nestUnder', { + defaultMessage: 'Nest under', + })} + + setColumns(nestColumn(layer.columnOrder, e.target.value, columnId))} + /> + + + ); +} 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 88763667a915f..ebf87fb1bdebb 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 @@ -32,6 +32,7 @@ import { operationDefinitionMap, getOperationDisplay, buildColumn } from '../ope import { deleteColumn, changeColumn } from '../state_helpers'; import { FieldSelect } from './field_select'; import { hasField } from '../utils'; +import { BucketNestingEditor } from './bucket_nesting_editor'; const operationPanels = getOperationDisplay(); @@ -363,6 +364,22 @@ export function PopoverEditor(props: PopoverEditorProps) { /> )} + { + setState({ + ...state, + layers: { + ...state.layers, + [props.layerId]: { + ...state.layers[props.layerId], + columnOrder, + }, + }, + }); + }} + />