diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index 12f384081fe7a..6beb75e3b4dbb 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -1,7 +1,7 @@ // 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'; @import './indexpattern_plugin/indexpattern'; -@import './editor_frame_plugin/editor_frame/index'; \ No newline at end of file +@import './drag_drop/drag_drop.scss'; +@import './editor_frame_plugin/editor_frame/index'; 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 9c07b07ae27fa..0602feff52d95 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 @@ -7,7 +7,7 @@ const actual = jest.requireActual('../operations'); jest.spyOn(actual, 'getPotentialColumns'); -jest.spyOn(actual.operationDefinitionMap.date_histogram, 'inlineOptions'); +jest.spyOn(actual.operationDefinitionMap.date_histogram, 'paramEditor'); export const { getPotentialColumns, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_index.scss new file mode 100644 index 0000000000000..919fe52748684 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_index.scss @@ -0,0 +1,2 @@ +@import './popover'; +@import './summary'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_popover.scss b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_popover.scss new file mode 100644 index 0000000000000..b5701daf31d7e --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_popover.scss @@ -0,0 +1,30 @@ +.lnsConfigPanel__summaryPopoverLeft, +.lnsConfigPanel__summaryPopoverRight { + padding: $euiSizeS; +} + +.lnsConfigPanel__summaryPopoverLeft { + padding-top: 0; + background-color: $euiColorLightestShade; +} + +.lnsConfigPanel__summaryPopoverRight { + width: $euiSize * 20; +} + +.lnsConfigPanel__fieldOption--incompatible { + color: $euiColorLightShade; +} + +.lnsConfigPanel__operation { + padding: $euiSizeXS; + font-size: 0.875rem; +} + +.lnsConfigPanel__operation--selected { + background-color: $euiColorLightShade; +} + +.lnsConfigPanel__operation--incompatible { + opacity: 0.7; +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_summary.scss b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_summary.scss new file mode 100644 index 0000000000000..f16b2acc8ba03 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_summary.scss @@ -0,0 +1,32 @@ +.lnsConfigPanel__summary { + @include euiFontSizeS; + background: $euiColorEmptyShade; + border-radius: $euiBorderRadius; + display: flex; + align-items: center; + margin-top: $euiSizeXS; + padding: $euiSizeS; +} + +.lnsConfigPanel__summaryPopover { + flex-grow: 1; + line-height: 0; + overflow: hidden; +} + +.lnsConfigPanel__summaryPopoverAnchor { + max-width: 100%; +} + +.lnsConfigPanel__summaryIcon { + margin-right: $euiSizeXS; +} + +.lnsConfigPanel__summaryLink { + max-width: 100%; + display: flex; +} + +.lnsConfigPanel__summaryField { + color: $euiColorPrimary; +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index 863bd005a5a5c..b5bd083472566 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { ReactWrapper, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { EuiComboBox, EuiContextMenuItem } from '@elastic/eui'; +import { EuiComboBox, EuiSideNav, EuiPopover } from '@elastic/eui'; import { IndexPatternPrivateState } from '../indexpattern'; import { changeColumn } from '../state_helpers'; -import { getPotentialColumns, operationDefinitionMap } from '../operations'; -import { IndexPatternDimensionPanel } from './dimension_panel'; +import { getPotentialColumns } from '../operations'; +import { IndexPatternDimensionPanel, IndexPatternDimensionPanelProps } from './dimension_panel'; import { DropHandler, DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; +import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; jest.mock('../state_helpers'); jest.mock('../operations'); @@ -35,6 +36,12 @@ const expectedIndexPatterns = { aggregatable: true, searchable: true, }, + { + name: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, { name: 'source', type: 'string', @@ -46,9 +53,19 @@ const expectedIndexPatterns = { }; describe('IndexPatternDimensionPanel', () => { + let wrapper: ReactWrapper | ShallowWrapper; let state: IndexPatternPrivateState; + let setState: jest.Mock; + let defaultProps: IndexPatternDimensionPanelProps; let dragDropContext: DragContextState; + function openPopover() { + wrapper + .find('[data-test-subj="indexPattern-configure-dimension"]') + .first() + .simulate('click'); + } + beforeEach(() => { state = { indexPatterns: expectedIndexPatterns, @@ -71,21 +88,29 @@ describe('IndexPatternDimensionPanel', () => { }, }; + setState = jest.fn(); + dragDropContext = createMockedDragDropContext(); + defaultProps = { + dragDropContext, + state, + setState, + columnId: 'col1', + filterOperations: () => true, + }; + jest.clearAllMocks(); }); + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + }); + it('should display a configure button if dimension has no column yet', () => { - const wrapper = mount( - {}} - columnId={'col2'} - filterOperations={() => true} - /> - ); + wrapper = mount(); expect( wrapper .find('[data-test-subj="indexPattern-configure-dimension"]') @@ -95,16 +120,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should pass the right arguments to getPotentialColumns', async () => { - shallow( - {}} - columnId={'col1'} - filterOperations={() => true} - suggestedPriority={1} - /> - ); + wrapper = shallow(); expect(getPotentialColumns as jest.Mock).toHaveBeenCalledWith(state, 1); }); @@ -112,154 +128,93 @@ describe('IndexPatternDimensionPanel', () => { it('should call the filterOperations function', () => { const filterOperations = jest.fn().mockReturnValue(true); - shallow( - {}} - columnId={'col2'} - filterOperations={filterOperations} - /> + wrapper = shallow( + ); expect(filterOperations).toBeCalled(); }); it('should show field select combo box on click', () => { - const wrapper = mount( - {}} - columnId={'col2'} - filterOperations={() => true} - /> - ); + wrapper = mount(); - wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() - .simulate('click'); + openPopover(); expect(wrapper.find(EuiComboBox)).toHaveLength(1); }); it('should not show any choices if the filter returns false', () => { - const wrapper = mount( + wrapper = mount( {}} + {...defaultProps} columnId={'col2'} filterOperations={() => false} /> ); - wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() - .simulate('click'); + openPopover(); expect(wrapper.find(EuiComboBox)!.prop('options')!).toHaveLength(0); }); - it('should render the inline options directly', () => { - mount( - {}} - columnId={'col1'} - filterOperations={() => false} - /> - ); + it('should list all field names and document as a whole in prioritized order', () => { + wrapper = mount(); - expect(operationDefinitionMap.date_histogram.inlineOptions as jest.Mock).toHaveBeenCalledTimes( - 1 - ); - }); + openPopover(); - it('should not render the settings button if there are no settings or options', () => { - const wrapper = mount( - {}} - columnId={'col1'} - filterOperations={() => false} - /> - ); + const options = wrapper.find(EuiComboBox).prop('options'); + + expect(options![0].label).toEqual('Document'); - expect(wrapper.find('[data-test-subj="indexPattern-dimensionPopover-button"]')).toHaveLength(0); + expect(options![1].options!.map(({ label }) => label)).toEqual([ + 'timestamp', + 'bytes', + 'memory', + 'source', + ]); }); - it('should render the settings button if there are settings', () => { - const wrapper = mount( + it('should indicate fields which are imcompatible for the operation of the current column', () => { + 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( - {}} - columnId={'col1'} - filterOperations={() => true} /> ); - wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() - .simulate('click'); + openPopover(); const options = wrapper.find(EuiComboBox).prop('options'); - expect(options![0].label).toEqual('Document'); + expect(options![0].className).toContain('incompatible'); - expect(options![1].options!.map(({ label }) => label)).toEqual([ - 'bytes', - 'source', - 'timestamp', - ]); + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0].className + ).toContain('incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'memory')[0].className + ).not.toContain('incompatible'); }); - it('should show all functions that work with the current column', () => { - const setState = jest.fn(); - - const wrapper = mount( + it('should indicate operations which are incompatible for the field of the current column', () => { + wrapper = mount( { }, }, }} - setState={setState} - columnId={'col1'} - filterOperations={() => true} /> ); - wrapper - .find('[data-test-subj="indexPattern-dimensionPopover-button"]') - .first() - .simulate('click'); + openPopover(); - expect(wrapper.find(EuiContextMenuItem).map(instance => instance.text())).toEqual([ - 'Minimum', - 'Maximum', - 'Average', - 'Sum', - ]); + const options = (wrapper.find(EuiSideNav).prop('items')[0].items as unknown) as Array<{ + name: string; + className: string; + }>; + + expect(options.find(({ name }) => name === 'Minimum')!.className).not.toContain('incompatible'); + + expect(options.find(({ name }) => name === 'Date Histogram')!.className).toContain( + 'incompatible' + ); + }); + + it('should keep the operation when switching to another field compatible with this operation', () => { + const initialState: IndexPatternPrivateState = { + ...state, + columns: { + ...state.columns, + col1: { + operationId: 'op1', + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'max', + sourceField: 'bytes', + }, + }, + }; + + wrapper = mount(); + + openPopover(); + + const comboBox = wrapper.find(EuiComboBox)!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'memory')!; + + comboBox.prop('onChange')!([option]); + + expect(setState).toHaveBeenCalledWith({ + ...initialState, + columns: { + ...state.columns, + col1: expect.objectContaining({ + operationType: 'max', + sourceField: 'memory', + // Other parts of this don't matter for this test + }), + }, + }); }); - it('should update the datasource state on selection of an operation', () => { - const setState = jest.fn(); + it('should switch operations when selecting a field that requires another operation', () => { + wrapper = mount(); + + openPopover(); - const wrapper = mount( + const comboBox = wrapper.find(EuiComboBox)!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; + + 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 keep the field when switching to another operation compatible for this field', () => { + wrapper = mount( { }, }, }} - setState={setState} - columnId={'col1'} - filterOperations={() => true} - suggestedPriority={1} /> ); - wrapper - .find('[data-test-subj="indexPattern-dimensionPopover-button"]') - .first() - .simulate('click'); + openPopover(); - wrapper - .find('[data-test-subj="lns-indexPatternDimension-min"]') - .first() - .simulate('click'); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); expect(setState).toHaveBeenCalledWith({ ...state, @@ -347,61 +351,203 @@ describe('IndexPatternDimensionPanel', () => { }); }); - it('should update the datasource state on selection of a field', () => { - const setState = jest.fn(); + it('should not set the state if selecting the currently active operation', () => { + wrapper = mount(); - const wrapper = mount( - true} - suggestedPriority={1} - /> - ); + openPopover(); wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') .simulate('click'); - const comboBox = wrapper.find(EuiComboBox)!; - const option = comboBox.prop('options')![1].options![1]; + expect(setState).not.toHaveBeenCalled(); + }); - comboBox.prop('onChange')!([option]); + it('should update label on label input changes', () => { + wrapper = mount(); + + openPopover(); + + wrapper + .find('input[data-test-subj="indexPattern-label-edit"]') + .simulate('change', { target: { value: 'New Label' } }); expect(setState).toHaveBeenCalledWith({ ...state, columns: { ...state.columns, col1: expect.objectContaining({ - operationType: 'terms', - sourceField: 'source', + label: 'New Label', // Other parts of this don't matter for this test }), }, }); }); - it('should add a column on selection of a field', () => { - const setState = jest.fn(); + describe('transient invalid state', () => { + it('should not set the state if selecting an operation incompatible with the current field', () => { + wrapper = mount(); + + openPopover(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-terms"]').simulate('click'); + + expect(setState).not.toHaveBeenCalled(); + }); + + it('should show error message in invalid state', () => { + wrapper = mount(); + + openPopover(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-terms"]').simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).not.toHaveLength(0); + + expect(setState).not.toHaveBeenCalled(); + }); + + it('should leave error state if a compatible operation is selected', () => { + wrapper = mount(); + + openPopover(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-terms"]').simulate('click'); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); + + it('should leave error state if the popover gets closed', () => { + wrapper = mount(); + + openPopover(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-terms"]').simulate('click'); + + wrapper.find(EuiPopover).prop('closePopover')!(); + + openPopover(); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); + + it('should indicate fields compatible with selected operation', () => { + wrapper = mount(); + + openPopover(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-terms"]').simulate('click'); + + const options = wrapper.find(EuiComboBox).prop('options'); + + expect(options![0].className).toContain('incompatible'); + + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0].className + ).toContain('incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'source')[0].className + ).not.toContain('incompatible'); + }); + + it('should set datasource state if compatible field is selected for operation', () => { + wrapper = mount(); + + openPopover(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-terms"]').simulate('click'); + + const comboBox = wrapper.find(EuiComboBox)!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; + + comboBox.prop('onChange')!([option]); + + expect(setState).toHaveBeenCalledWith({ + ...state, + columns: { + col1: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + }), + }, + }); + }); + }); + + it('should support selecting the operation before the field', () => { + wrapper = mount(); + + openPopover(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + + const comboBox = wrapper.find(EuiComboBox); + const options = comboBox.prop('options'); + + comboBox.prop('onChange')!([options![1].options![0]]); + + expect(setState).toHaveBeenCalledWith({ + ...state, + columns: { + ...state.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'avg', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], + }); + }); + + it('should indicate compatible fields when selecting the operation first', () => { + wrapper = mount(); + + openPopover(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + + const options = wrapper.find(EuiComboBox).prop('options'); - const wrapper = mount( + expect(options![0].className).toContain('incompatible'); + + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0].className + ).toContain('incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'bytes')[0].className + ).not.toContain('incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'memory')[0].className + ).not.toContain('incompatible'); + }); + + it('should show all operations that are not filtered out', () => { + wrapper = mount( true} - suggestedPriority={1} + {...defaultProps} + filterOperations={op => !op.isBucketed && op.dataType === 'number'} /> ); - wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() - .simulate('click'); + openPopover(); + + expect( + wrapper + .find(EuiSideNav) + .prop('items')[0] + .items.map(({ name }) => name) + ).toEqual(['Count', 'Maximum', 'Average', 'Sum', 'Minimum']); + }); + + it('should add a column on selection of a field', () => { + wrapper = mount(); + + openPopover(); const comboBox = wrapper.find(EuiComboBox)!; const option = comboBox.prop('options')![1].options![0]; @@ -422,46 +568,33 @@ describe('IndexPatternDimensionPanel', () => { }); it('should use helper function when changing the function', () => { - const setState = jest.fn(); - - const wrapper = mount( - true} - suggestedPriority={1} - /> - ); + // Private + operationType: 'max', + sourceField: 'bytes', + }, + }, + }; + wrapper = mount(); - wrapper - .find('[data-test-subj="indexPattern-dimensionPopover-button"]') - .first() - .simulate('click'); + openPopover(); wrapper .find('[data-test-subj="lns-indexPatternDimension-min"]') .first() - .simulate('click'); + .prop('onClick')!({} as React.MouseEvent<{}, MouseEvent>); expect(changeColumn).toHaveBeenCalledWith( - expect.anything(), + initialState, 'col1', expect.objectContaining({ sourceField: 'bytes', @@ -471,17 +604,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should clear the dimension with the clear button', () => { - const setState = jest.fn(); - - const wrapper = mount( - true} - /> - ); + wrapper = mount(); const clearButton = wrapper.find( 'EuiButtonIcon[data-test-subj="indexPattern-dimensionPopover-remove"]' @@ -496,6 +619,20 @@ describe('IndexPatternDimensionPanel', () => { }); }); + it('should clear the dimension when removing the selection in field combobox', () => { + wrapper = mount(); + + openPopover(); + + wrapper.find(EuiComboBox).prop('onChange')!([]); + + expect(setState).toHaveBeenCalledWith({ + ...state, + columns: {}, + columnOrder: [], + }); + }); + describe('drag and drop', () => { function dragDropState() { return { @@ -512,18 +649,10 @@ describe('IndexPatternDimensionPanel', () => { } it('is not droppable if no drag is happening', () => { - const component = mount( - {}} - columnId={'col2'} - filterOperations={() => true} - /> - ); + wrapper = mount(); expect( - component + wrapper .find('[data-test-subj="indexPattern-dropTarget"]') .first() .prop('droppable') @@ -531,21 +660,19 @@ describe('IndexPatternDimensionPanel', () => { }); it('is not droppable if the dragged item has no type', () => { - const component = shallow( + wrapper = shallow( {}} - columnId={'col2'} - filterOperations={() => true} /> ); expect( - component + wrapper .find('[data-test-subj="indexPattern-dropTarget"]') .first() .prop('droppable') @@ -553,21 +680,20 @@ describe('IndexPatternDimensionPanel', () => { }); it('is not droppable if field is not supported by filterOperations', () => { - const component = shallow( + wrapper = shallow( {}} - columnId={'col2'} filterOperations={() => false} /> ); expect( - component + wrapper .find('[data-test-subj="indexPattern-dropTarget"]') .first() .prop('droppable') @@ -575,21 +701,20 @@ describe('IndexPatternDimensionPanel', () => { }); it('is droppable if the field is supported by filterOperations', () => { - const component = shallow( + wrapper = shallow( {}} - columnId={'col2'} filterOperations={op => op.dataType === 'number'} /> ); expect( - component + wrapper .find('[data-test-subj="indexPattern-dropTarget"]') .first() .prop('droppable') @@ -599,21 +724,20 @@ describe('IndexPatternDimensionPanel', () => { it('appends the dropped column when a field is dropped', () => { const dragging = { type: 'number', name: 'bar' }; const testState = dragDropState(); - const setState = jest.fn(); - const component = shallow( + wrapper = shallow( op.dataType === 'number'} /> ); - const onDrop = component + const onDrop = wrapper .find('[data-test-subj="indexPattern-dropTarget"]') .first() .prop('onDrop') as DropHandler; @@ -637,21 +761,19 @@ describe('IndexPatternDimensionPanel', () => { it('updates a column when a field is dropped', () => { const dragging = { type: 'number', name: 'bar' }; const testState = dragDropState(); - const setState = jest.fn(); - const component = shallow( + wrapper = shallow( op.dataType === 'number'} /> ); - const onDrop = component + const onDrop = wrapper .find('[data-test-subj="indexPattern-dropTarget"]') .first() .prop('onDrop') as DropHandler; @@ -671,24 +793,22 @@ describe('IndexPatternDimensionPanel', () => { ); }); - it('ignores drops of unsupported fields', () => { + it('ignores drops of incompatible fields', () => { const dragging = { type: 'number', name: 'baz' }; const testState = dragDropState(); - const setState = jest.fn(); - const component = shallow( + wrapper = shallow( op.dataType === 'number'} /> ); - const onDrop = component + const onDrop = wrapper .find('[data-test-subj="indexPattern-dropTarget"]') .first() .prop('onDrop') as DropHandler; 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 bd318be148e17..2671da17bfd3a 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 @@ -6,7 +6,8 @@ import _ from 'lodash'; import React from 'react'; -import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { DatasourceDimensionPanelProps } from '../../types'; import { IndexPatternColumn, @@ -15,11 +16,10 @@ import { IndexPatternField, } from '../indexpattern'; -import { getPotentialColumns, operationDefinitionMap } from '../operations'; -import { FieldSelect } from './field_select'; -import { Settings } from './settings'; +import { getPotentialColumns } from '../operations'; +import { PopoverEditor } from './popover_editor'; import { DragContextState, ChildDragDropProvider, DragDrop } from '../../drag_drop'; -import { changeColumn, hasField } from '../state_helpers'; +import { changeColumn, hasField, deleteColumn } from '../state_helpers'; export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { state: IndexPatternPrivateState; @@ -36,9 +36,6 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp const selectedColumn: IndexPatternColumn | null = props.state.columns[props.columnId] || null; - const ParamEditor = - selectedColumn && operationDefinitionMap[selectedColumn.operationType].inlineOptions; - function findColumnByField(field: IndexPatternField) { return filteredColumns.find(col => hasField(col) && col.sourceField === field.name); } @@ -53,6 +50,7 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp return ( { @@ -66,30 +64,32 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp props.setState(changeColumn(props.state, props.columnId, column)); }} > - + + + + - - - - + {selectedColumn && ( + + { + props.setState(deleteColumn(props.state, props.columnId)); + }} + /> + + )} - {ParamEditor && ( - - - - )} 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 b14f9503dadf3..9cb3232aeb295 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 @@ -5,37 +5,54 @@ */ import _ from 'lodash'; -import React, { useState } from 'react'; +import React from 'react'; 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, hasField, sortByField } from '../state_helpers'; +import { EuiComboBox } from '@elastic/eui'; +import classNames from 'classnames'; +import { + IndexPatternColumn, + FieldBasedIndexPatternColumn, + OperationType, + BaseIndexPatternColumn, +} from '../indexpattern'; +import { hasField, sortByField } from '../state_helpers'; -export interface FieldSelectProps extends IndexPatternDimensionPanelProps { - selectedColumn: IndexPatternColumn; +export interface FieldSelectProps { + incompatibleSelectedOperationType: OperationType | null; + selectedColumn?: IndexPatternColumn; filteredColumns: IndexPatternColumn[]; + onChangeColumn: (newColumn: IndexPatternColumn) => void; + onDeleteColumn: () => void; } export function FieldSelect({ + incompatibleSelectedOperationType, selectedColumn, filteredColumns, - state, - columnId, - setState, + onChangeColumn, + onDeleteColumn, }: FieldSelectProps) { - const [isFieldSelectOpen, setFieldSelectOpen] = useState(false); const fieldColumns = filteredColumns.filter(hasField) as FieldBasedIndexPatternColumn[]; const uniqueColumnsByField = sortByField( _.uniq( fieldColumns - .filter(col => selectedColumn && col.operationType === selectedColumn.operationType) + .filter(col => + incompatibleSelectedOperationType + ? col.operationType === incompatibleSelectedOperationType + : selectedColumn && col.operationType === selectedColumn.operationType + ) .concat(fieldColumns), col => col.sourceField ) ); + function isCompatibleWithCurrentOperation(col: BaseIndexPatternColumn) { + return incompatibleSelectedOperationType + ? col.operationType === incompatibleSelectedOperationType + : !selectedColumn || col.operationType === selectedColumn.operationType; + } + const fieldOptions = []; const fieldLessColumn = filteredColumns.find(column => !hasField(column)); if (fieldLessColumn) { @@ -44,6 +61,11 @@ export function FieldSelect({ defaultMessage: 'Document', }), value: fieldLessColumn.operationId, + className: classNames({ + 'lnsConfigPanel__fieldOption--incompatible': !isCompatibleWithCurrentOperation( + fieldLessColumn + ), + }), }); } @@ -52,88 +74,63 @@ export function FieldSelect({ label: i18n.translate('xpack.lens.indexPattern.individualFieldsLabel', { defaultMessage: 'Individual fields', }), - options: uniqueColumnsByField.map(col => ({ - label: col.sourceField, - value: col.operationId, - })), + options: uniqueColumnsByField + .map(col => ({ + label: col.sourceField, + value: col.operationId, + compatible: isCompatibleWithCurrentOperation(col), + })) + .sort(({ compatible: a }, { compatible: b }) => { + if (a && !b) { + return -1; + } + if (!a && b) { + return 1; + } + return 0; + }) + .map(({ label, value, compatible }) => ({ + label, + value, + className: classNames({ 'lnsConfigPanel__fieldOption--incompatible': !compatible }), + })), }); } 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={i18n.translate('xpack.lens.indexPattern.fieldPlaceholder', { - defaultMessage: 'Field', - })} - options={fieldOptions} - selectedOptions={ - selectedColumn && hasField(selectedColumn) - ? [ - { - label: selectedColumn.sourceField, - value: selectedColumn.operationId, - }, - ] - : [] - } - singleSelection={{ asPlainText: true }} - isClearable={true} - onChange={choices => { - setFieldSelectOpen(false); - - if (choices.length === 0) { - setState(deleteColumn(state, columnId)); - return; - } + { + if (choices.length === 0) { + onDeleteColumn(); + return; + } - const column: IndexPatternColumn = filteredColumns.find( - ({ operationId }) => operationId === choices[0].value - )!; + const column: IndexPatternColumn = filteredColumns.find( + ({ operationId }) => operationId === choices[0].value + )!; - setState(changeColumn(state, columnId, column)); - }} - /> - )} - - {selectedColumn && ( - - { - setState(deleteColumn(state, columnId)); - }} - /> - - )} - + onChangeColumn(column); + }} + /> ); } 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 new file mode 100644 index 0000000000000..2254f1b474644 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx @@ -0,0 +1,206 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiPopover, + EuiFlexItem, + EuiFlexGroup, + EuiSideNav, + EuiCallOut, + EuiFormRow, + EuiFieldText, + EuiLink, +} from '@elastic/eui'; +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 { FieldSelect } from './field_select'; + +const operationPanels = getOperationDisplay(); + +function getOperationTypes( + filteredColumns: IndexPatternColumn[], + selectedColumn?: IndexPatternColumn +) { + const columnsFromField = selectedColumn + ? filteredColumns.filter(col => { + return ( + (!hasField(selectedColumn) && !hasField(col)) || + (hasField(selectedColumn) && + hasField(col) && + col.sourceField === selectedColumn.sourceField) + ); + }) + : filteredColumns; + const possibleOperationTypes = filteredColumns.map(col => ({ + operationType: col.operationType, + compatibleWithCurrentField: false, + })); + const validOperationTypes = columnsFromField.map(col => ({ + operationType: col.operationType, + compatibleWithCurrentField: true, + })); + return _.uniq([...validOperationTypes, ...possibleOperationTypes], 'operationType'); +} + +export interface PopoverEditorProps extends IndexPatternDimensionPanelProps { + selectedColumn?: IndexPatternColumn; + filteredColumns: IndexPatternColumn[]; +} + +export function PopoverEditor(props: PopoverEditorProps) { + const { selectedColumn, filteredColumns, state, columnId, setState } = props; + const [isPopoverOpen, setPopoverOpen] = useState(false); + const [ + incompatibleSelectedOperationType, + setInvalidOperationType, + ] = useState(null); + + const ParamEditor = + selectedColumn && operationDefinitionMap[selectedColumn.operationType].paramEditor; + + const sideNavItems = [ + { + name: '', + id: '0', + items: getOperationTypes(filteredColumns, selectedColumn).map( + ({ operationType, compatibleWithCurrentField }) => ({ + name: operationPanels[operationType].displayName, + id: operationType as string, + className: classNames('lnsConfigPanel__operation', { + 'lnsConfigPanel__operation--selected': Boolean( + incompatibleSelectedOperationType === operationType || + (!incompatibleSelectedOperationType && + selectedColumn && + selectedColumn.operationType === operationType) + ), + 'lnsConfigPanel__operation--incompatible': !compatibleWithCurrentField, + }), + 'data-test-subj': `lns-indexPatternDimension-${operationType}`, + onClick() { + if (!selectedColumn || !compatibleWithCurrentField) { + setInvalidOperationType(operationType); + return; + } + if (incompatibleSelectedOperationType) { + setInvalidOperationType(null); + } + if (selectedColumn.operationType === operationType) { + return; + } + const newColumn: IndexPatternColumn = filteredColumns.find( + col => + col.operationType === operationType && + (!hasField(col) || + !hasField(selectedColumn) || + col.sourceField === selectedColumn.sourceField) + )!; + setState(changeColumn(state, columnId, newColumn)); + }, + }) + ), + }, + ]; + + return ( + { + setPopoverOpen(true); + }} + data-test-subj="indexPattern-configure-dimension" + > + {selectedColumn + ? selectedColumn.label + : i18n.translate('xpack.lens.indexPattern.configureDimensionLabel', { + defaultMessage: 'Configure dimension', + })} + + } + isOpen={isPopoverOpen} + closePopover={() => { + setPopoverOpen(false); + setInvalidOperationType(null); + }} + anchorPosition="leftUp" + withTitle + panelPaddingSize="s" + > + + + { + setState(deleteColumn(state, columnId)); + }} + onChangeColumn={column => { + setState(changeColumn(state, columnId, column)); + setInvalidOperationType(null); + }} + /> + + + + + + + + {incompatibleSelectedOperationType && selectedColumn && ( + +

+ +

+
+ )} + {!incompatibleSelectedOperationType && ParamEditor && ( + + )} + {!incompatibleSelectedOperationType && selectedColumn && ( + + { + setState( + changeColumn(state, columnId, { + ...selectedColumn, + label: e.target.value, + }) + ); + }} + /> + + )} +
+
+
+
+
+ ); +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx deleted file mode 100644 index b9f0cf771a34a..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/settings.tsx +++ /dev/null @@ -1,111 +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 { - EuiPopover, - EuiButtonIcon, - EuiFlexItem, - EuiContextMenuItem, - EuiContextMenuPanel, -} from '@elastic/eui'; -import { IndexPatternColumn } from '../indexpattern'; -import { IndexPatternDimensionPanelProps } from './dimension_panel'; -import { operationDefinitionMap, getOperations, getOperationDisplay } from '../operations'; -import { changeColumn, hasField } from '../state_helpers'; - -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 ( - (!hasField(selectedColumn) && !hasField(col)) || - (hasField(selectedColumn) && - hasField(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 && - (!hasField(col) || - !hasField(selectedColumn) || - col.sourceField === selectedColumn.sourceField) - )!; - - setState(changeColumn(state, columnId, newColumn)); - }} - > - {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={i18n.translate('xpack.lens.indexPattern.settingsLabel', { - defaultMessage: 'Settings', - })} - /> - - } - > - {operationMenuItems.concat(contextOptions)} - - - ) : null; -} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.scss b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.scss index ac1b7d4ab754b..877afd3fcbbc4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.scss @@ -1,3 +1,5 @@ +@import './dimension_panel/index'; + .lnsIndexPattern__dimensionPopover { max-width: 600px; } 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 94cd3e6495672..59b463a545651 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 @@ -12,7 +12,7 @@ import { EuiRange } from '@elastic/eui'; describe('date_histogram', () => { let state: IndexPatternPrivateState; - const InlineOptions = dateHistogramOperation.inlineOptions!; + const InlineOptions = dateHistogramOperation.paramEditor!; beforeEach(() => { state = { 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 35a7de3ef6b6e..753a950f07cdd 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 @@ -92,7 +92,7 @@ export const dateHistogramOperation: OperationDefinition { + paramEditor: ({ state, setState, columnId }) => { const column = state.columns[columnId] as DateHistogramIndexPatternColumn; const field = @@ -112,7 +112,6 @@ export const dateHistogramOperation: OperationDefinition { let state: IndexPatternPrivateState; - const InlineOptions = termsOperation.inlineOptions!; - const contextMenu = termsOperation.contextMenu!; + const InlineOptions = termsOperation.paramEditor!; beforeEach(() => { state = { @@ -69,10 +68,9 @@ describe('terms', () => { 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( + ); - const instance = shallow(); expect(instance.find(EuiSelect).prop('value')).toEqual('alphabetical'); expect( @@ -85,10 +83,9 @@ describe('terms', () => { it('should update state with the order value', () => { const setStateSpy = jest.fn(); - const PartialMenu = () => ( - <>{contextMenu({ state, setState: setStateSpy, columnId: 'col1' })}/> + const instance = shallow( + ); - const instance = shallow(); instance.find(EuiSelect).prop('onChange')!({ target: { @@ -113,9 +110,7 @@ describe('terms', () => { }, }); }); - }); - describe('inline param editor', () => { it('should render current 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 d8a98eba90f86..98e3443549ced 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 @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiForm, EuiFormRow, EuiRange, EuiSelect, EuiContextMenuItem } from '@elastic/eui'; +import { EuiForm, EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; import { IndexPatternField, TermsIndexPatternColumn } from '../indexpattern'; import { DimensionPriority } from '../../types'; import { OperationDefinition } from '../operations'; @@ -78,7 +78,7 @@ export const termsOperation: OperationDefinition = { missingBucketLabel: 'Missing', }, }), - contextMenu: ({ state, setState, columnId: currentColumnId }) => { + paramEditor: ({ state, setState, columnId: currentColumnId }) => { const currentColumn = state.columns[currentColumnId] as TermsIndexPatternColumn; const SEPARATOR = '$$$'; function toValue(orderBy: TermsIndexPatternColumn['params']['orderBy']) { @@ -113,31 +113,6 @@ export const termsOperation: OperationDefinition = { defaultMessage: 'Alphabetical', }), }); - return [ - - - ) => - setState( - updateColumnParam(state, currentColumn, 'orderBy', fromValue(e.target.value)) - ) - } - /> - - , - ]; - }, - inlineOptions: ({ state, setState, columnId: currentColumnId }) => { - const currentColumn = state.columns[currentColumnId] as TermsIndexPatternColumn; return ( = { })} /> + + ) => + setState( + updateColumnParam(state, currentColumn, 'orderBy', fromValue(e.target.value)) + ) + } + aria-label={i18n.translate('xpack.lens.indexPattern.terms.orderBy', { + defaultMessage: 'Order by', + })} + /> + ); }, 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 20edaf338fd61..b5a0591084599 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts @@ -58,6 +58,7 @@ export interface ParamEditorProps { setState: (newState: IndexPatternPrivateState) => void; columnId: string; } + export interface OperationDefinition { type: C['operationType']; displayName: string; @@ -69,8 +70,7 @@ export interface OperationDefinition { suggestedOrder?: DimensionPriority, field?: IndexPatternField ) => C; - inlineOptions?: React.ComponentType; - contextMenu?: (props: ParamEditorProps) => JSX.Element[]; + paramEditor?: React.ComponentType; toEsAggsConfig: (column: C, columnId: string) => unknown; } 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 88ba0b923d6ee..c021119323b11 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 @@ -89,6 +89,48 @@ describe('state_helpers', () => { }) ); }); + + it('should carry over params from old column if the operation type stays the same', () => { + const state: IndexPatternPrivateState = { + indexPatterns: {}, + currentIndexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'h', + }, + }, + }, + }; + expect( + changeColumn(state, 'col2', { + operationId: 'op2', + label: 'Date histogram of order_date', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'order_date', + params: { + interval: 'w', + }, + }).columns.col1 + ).toEqual( + expect.objectContaining({ + params: { interval: 'h' }, + }) + ); + }); }); describe('getColumnOrder', () => { 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 aec5bf5a3b7b4..0ee3222c1e5d7 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 @@ -64,11 +64,22 @@ export function updateColumnParam< export function changeColumn( state: IndexPatternPrivateState, columnId: string, - newColumn: IndexPatternColumn + newColumn: IndexPatternColumn, + { keepParams }: { keepParams: boolean } = { keepParams: true } ) { + const oldColumn = state.columns[columnId]; + + const updatedColumn = + keepParams && + oldColumn && + oldColumn.operationType === newColumn.operationType && + 'params' in oldColumn + ? ({ ...newColumn, params: oldColumn.params } as IndexPatternColumn) + : newColumn; + const newColumns: IndexPatternPrivateState['columns'] = { ...state.columns, - [columnId]: newColumn, + [columnId]: updatedColumn, }; return {