diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx index 375b826cf3e3d..0b252729a9966 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx @@ -68,7 +68,7 @@ const PreviewRenderer = ({ }, [expression]); return expressionError ? ( -
+
) : ( { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss index 76f6c767b8b1e..cd2c9bff6b165 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss @@ -13,7 +13,7 @@ & > .lnsInnerIndexPatternDataPanel__changeLink { flex: 0 0 auto; - margin: 0 $euiSize; + margin: 0 (-$euiSizeS) 0 $euiSizeXS; } } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx new file mode 100644 index 0000000000000..3da540e194dc3 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx @@ -0,0 +1,126 @@ +/* + * 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 { + EuiButtonEmpty, + EuiIconTip, + EuiPopover, + EuiSelectable, + EuiButtonEmptyProps, +} from '@elastic/eui'; +import { EuiSelectableProps } from '@elastic/eui/src/components/selectable/selectable'; +import { i18n } from '@kbn/i18n'; +import { IndexPatternPrivateState, IndexPatternLayer } from './indexpattern'; +import { isLayerTransferable } from './state_helpers'; + +export interface ChangeIndexPatternTriggerProps extends EuiButtonEmptyProps { + label: string; +} + +export function ChangeIndexPattern({ + indexPatterns, + currentIndexPatternId, + onChangeIndexPattern, + trigger, + layer, + selectableProps, +}: { + trigger: ChangeIndexPatternTriggerProps; + indexPatterns: IndexPatternPrivateState['indexPatterns']; + onChangeIndexPattern: (newId: string) => void; + currentIndexPatternId?: string; + layer?: IndexPatternLayer; + selectableProps?: EuiSelectableProps; +}) { + const [isPopoverOpen, setPopoverIsOpen] = useState(false); + const [selectedID, setSelectedID] = useState( + layer ? layer.indexPatternId : currentIndexPatternId + ); + + const indexPatternList = Object.values(indexPatterns).map(indexPattern => ({ + ...indexPattern, + isTransferable: layer ? isLayerTransferable(layer, indexPattern) : undefined, + })); + + const createTrigger = function() { + const { label, ...rest } = trigger; + return ( + setPopoverIsOpen(!isPopoverOpen)} + {...rest} + > + {label} + + ); + }; + + return ( + <> + setPopoverIsOpen(false)} + className="eui-textTruncate" + anchorClassName="eui-textTruncate" + display="block" + panelPaddingSize="s" + ownFocus + > +
+ ({ + id: indexPattern.id, + label: indexPattern.title, + checked: indexPattern.id === selectedID ? 'on' : undefined, + append: + indexPattern && indexPattern.isTransferable !== false ? ( + undefined + ) : ( + + ), + }))} + onChange={choices => { + // @ts-ignore + const newSelectedID = choices.find(({ checked }) => checked)!.id; + onChangeIndexPattern(newSelectedID); + setSelectedID(newSelectedID); + setPopoverIsOpen(false); + }} + searchProps={{ + compressed: true, + ...(selectableProps ? selectableProps.searchProps : undefined), + }} + > + {(list, search) => ( + <> + {search} + {list} + + )} + +
+
+ + ); +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx index e1cd8fbe3b551..9e20db9276ae3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx @@ -6,13 +6,13 @@ import { shallow, mount } from 'enzyme'; import React, { ChangeEvent } from 'react'; -import { EuiComboBox } from '@elastic/eui'; import { IndexPatternPrivateState, IndexPatternColumn } from './indexpattern'; import { createMockedDragDropContext } from './mocks'; import { InnerIndexPatternDataPanel, IndexPatternDataPanel, MemoizedDataPanel } from './datapanel'; import { FieldItem } from './field_item'; import { act } from 'react-dom/test-utils'; import { coreMock } from 'src/core/public/mocks'; +import { ChangeIndexPattern } from './change_indexpattern'; jest.mock('ui/new_platform'); jest.mock('./loader'); @@ -213,8 +213,6 @@ describe('IndexPattern Data Panel', () => { dragDropContext: createMockedDragDropContext(), currentIndexPatternId: '1', indexPatterns: initialState.indexPatterns, - showIndexPatternSwitcher: false, - setShowIndexPatternSwitcher: jest.fn(), onChangeIndexPattern: jest.fn(), core, dateRange: { @@ -229,24 +227,22 @@ describe('IndexPattern Data Panel', () => { it('should update index pattern of layer on switch if it is a single empty one', async () => { const setStateSpy = jest.fn(); + const state = { + ...initialState, + layers: { first: { indexPatternId: '1', columnOrder: [], columns: {} } }, + }; const wrapper = shallow( {} }} /> ); - act(() => { - wrapper.find(MemoizedDataPanel).prop('setShowIndexPatternSwitcher')!(true); - }); wrapper.find(MemoizedDataPanel).prop('onChangeIndexPattern')!('2'); - expect(setStateSpy).toHaveBeenCalledWith({ + expect(setStateSpy.mock.calls[0][0](state)).toEqual({ ...initialState, layers: { first: { indexPatternId: '2', columnOrder: [], columns: {} } }, currentIndexPatternId: '2', @@ -271,12 +267,12 @@ describe('IndexPattern Data Panel', () => { /> ); - act(() => { - wrapper.find(MemoizedDataPanel).prop('setShowIndexPatternSwitcher')!(true); - }); wrapper.find(MemoizedDataPanel).prop('onChangeIndexPattern')!('2'); - expect(setStateSpy).toHaveBeenCalledWith({ ...state, currentIndexPatternId: '2' }); + expect(setStateSpy.mock.calls[0][0](state)).toEqual({ + ...state, + currentIndexPatternId: '2', + }); }); it('should not update index pattern of layer on switch if there are columns configured', async () => { @@ -300,12 +296,12 @@ describe('IndexPattern Data Panel', () => { /> ); - act(() => { - wrapper.find(MemoizedDataPanel).prop('setShowIndexPatternSwitcher')!(true); - }); wrapper.find(MemoizedDataPanel).prop('onChangeIndexPattern')!('2'); - expect(setStateSpy).toHaveBeenCalledWith({ ...state, currentIndexPatternId: '2' }); + expect(setStateSpy.mock.calls[0][0](state)).toEqual({ + ...state, + currentIndexPatternId: '2', + }); }); it('should render a warning if there are no index patterns', () => { @@ -318,20 +314,7 @@ describe('IndexPattern Data Panel', () => { it('should call setState when the index pattern is switched', async () => { const wrapper = shallow(); - wrapper.find('[data-test-subj="indexPattern-switch-link"]').simulate('click'); - - expect(defaultProps.setShowIndexPatternSwitcher).toHaveBeenCalledWith(true); - - wrapper.setProps({ showIndexPatternSwitcher: true }); - - const comboBox = wrapper.find(EuiComboBox); - - comboBox.prop('onChange')!([ - { - label: initialState.indexPatterns['2'].title, - value: '2', - }, - ]); + wrapper.find(ChangeIndexPattern).prop('onChangeIndexPattern')('2'); expect(defaultProps.onChangeIndexPattern).toHaveBeenCalledWith('2'); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx index cbb816ef6a968..12e6150a789b3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx @@ -7,14 +7,10 @@ import { mapValues, uniq, indexBy } from 'lodash'; import React, { useState, useEffect, memo, useCallback } from 'react'; import { - EuiComboBox, EuiLoadingSpinner, - // @ts-ignore - EuiHighlight, EuiFlexGroup, EuiFlexItem, EuiTitle, - EuiButtonEmpty, EuiContextMenuPanel, EuiContextMenuItem, EuiContextMenuPanelProps, @@ -36,6 +32,7 @@ import { ChildDragDropProvider, DragContextState } from '../drag_drop'; import { FieldItem } from './field_item'; import { FieldIcon } from './field_icon'; import { updateLayerIndexPattern } from './state_helpers'; +import { ChangeIndexPattern } from './change_indexpattern'; // TODO the typings for EuiContextMenuPanel are incorrect - watchedItemProps is missing. This can be removed when the types are adjusted const FixedEuiContextMenuPanel = (EuiContextMenuPanel as unknown) as React.FunctionComponent< @@ -71,21 +68,20 @@ export function IndexPatternDataPanel({ dateRange, }: DatasourceDataPanelProps) { const { indexPatterns, currentIndexPatternId } = state; - const [showIndexPatternSwitcher, setShowIndexPatternSwitcher] = useState(false); const onChangeIndexPattern = useCallback( (newIndexPattern: string) => { - setState({ - ...state, - layers: isSingleEmptyLayer(state.layers) - ? mapValues(state.layers, layer => + setState(prevState => ({ + ...prevState, + layers: isSingleEmptyLayer(prevState.layers) + ? mapValues(prevState.layers, layer => updateLayerIndexPattern(layer, indexPatterns[newIndexPattern]) ) - : state.layers, + : prevState.layers, currentIndexPatternId: newIndexPattern, - }); + })); }, - [state, setState] + [setState] ); const updateFieldsWithCounts = useCallback( @@ -109,12 +105,10 @@ export function IndexPatternDataPanel({ const onToggleEmptyFields = useCallback(() => { setState(prevState => ({ ...prevState, showEmptyFields: !prevState.showEmptyFields })); - }, [state, setState]); + }, [setState]); return ( void; showEmptyFields: boolean; onToggleEmptyFields: () => void; - onChangeIndexPattern?: (newId: string) => void; + onChangeIndexPattern: (newId: string) => void; updateFieldsWithCounts?: (indexPatternId: string, fields: IndexPattern['fields']) => void; }) { if (Object.keys(indexPatterns).length === 0) { @@ -335,70 +324,31 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ >
- {!showIndexPatternSwitcher ? ( - <> - -

- {currentIndexPattern.title}{' '} -

-
- setShowIndexPatternSwitcher(true)} - size="xs" - > - ( - - ) - - - ) : ( - +

{currentIndexPattern.title}

+ +
+ ({ - label: title, - value: id, - }))} - inputRef={el => { - if (el) { - el.focus(); - } - }} - selectedOptions={ - currentIndexPatternId - ? [ - { - label: currentIndexPattern.title, - value: currentIndexPattern.id, - }, - ] - : undefined - } - singleSelection={{ asPlainText: true }} - isClearable={false} - onBlur={() => { - setShowIndexPatternSwitcher(false); + trigger={{ + label: i18n.translate('xpack.lens.indexPatterns.changePatternLabel', { + defaultMessage: '(change)', + }), + 'data-test-subj': 'indexPattern-switch-link', }} - onChange={choices => { - onChangeIndexPattern!(choices[0].value as string); + currentIndexPatternId={currentIndexPatternId} + indexPatterns={indexPatterns} + onChangeIndexPattern={(newId: string) => { + onChangeIndexPattern(newId); setLocalState(s => ({ ...s, nameFilter: '', typeFilter: [], })); - - setShowIndexPatternSwitcher(false); }} /> - )} +
@@ -469,7 +419,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ /> { onToggleEmptyFields(); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx index 755ed02904e31..f54585e569fa5 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiComboBox } from '@elastic/eui'; +import React, { ReactElement } from 'react'; import { IndexPatternPrivateState } from './indexpattern'; -import { act } from 'react-dom/test-utils'; import { IndexPatternLayerPanelProps, LayerPanel } from './layerpanel'; import { updateLayerIndexPattern } from './state_helpers'; -import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; -import { ReactWrapper } from 'enzyme'; +import { shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; +import { ShallowWrapper } from 'enzyme'; +import { EuiSelectable, EuiSelectableList } from '@elastic/eui'; +import { ChangeIndexPattern } from './change_indexpattern'; jest.mock('ui/new_platform'); jest.mock('./state_helpers'); @@ -177,53 +177,61 @@ describe('Layer Data Panel', () => { }; }); - function clickLabel(instance: ReactWrapper) { - act(() => { + function getIndexPatternPickerList(instance: ShallowWrapper) { + return instance + .find(ChangeIndexPattern) + .first() + .dive() + .find(EuiSelectable); + } + + function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { + const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions( instance - .find('[data-test-subj="lns_layerIndexPatternLabel"]') - .first() - .simulate('click'); - }); + ).map(option => + option.label === selectedLabel + ? { ...option, checked: 'on' } + : { ...option, checked: undefined } + ); + return getIndexPatternPickerList(instance).prop('onChange')!(options); + } - instance.update(); + function getIndexPatternPickerOptions(instance: ShallowWrapper) { + return getIndexPatternPickerList(instance) + .dive() + .find(EuiSelectableList) + .prop('options'); } - it('should list all index patterns but the current one', () => { - const instance = mount(); - clickLabel(instance); + it('should list all index patterns', () => { + const instance = shallow(); - expect( - instance - .find(EuiComboBox) - .prop('options')! - .map(option => option.label) - ).toEqual(['my-fake-restricted-pattern', 'my-compatible-pattern']); + expect(getIndexPatternPickerOptions(instance)!.map(option => option.label)).toEqual([ + 'my-fake-index-pattern', + 'my-fake-restricted-pattern', + 'my-compatible-pattern', + ]); }); - it('should indicate whether the switch can be made without lossing data', () => { - const instance = mount(); - clickLabel(instance); + it('should indicate whether the switch can be made without losing data', () => { + const instance = shallow(); expect( - instance - .find(EuiComboBox) - .prop('options')! - .map(option => (option.value as { isTransferable: boolean }).isTransferable) - ).toEqual([false, true]); + getIndexPatternPickerOptions(instance)!.map(option => + Boolean( + option.append && + (option.append as ReactElement).props.content.includes( + 'Not all operations are compatible with this index pattern' + ) + ) + ) + ).toEqual([false, true, false]); }); it('should switch data panel to target index pattern', () => { - const instance = mount(); - clickLabel(instance); + const instance = shallow(); - act(() => { - instance.find(EuiComboBox).prop('onChange')!([ - { - label: 'my-compatible-pattern', - value: defaultProps.state.indexPatterns['3'], - }, - ]); - }); + selectIndexPatternPickerOption(instance, 'my-compatible-pattern'); expect(defaultProps.setState).toHaveBeenCalledWith( expect.objectContaining({ @@ -233,17 +241,8 @@ describe('Layer Data Panel', () => { }); it('should switch using updateLayerIndexPattern', () => { - const instance = mount(); - clickLabel(instance); - - act(() => { - instance.find(EuiComboBox).prop('onChange')!([ - { - label: 'my-compatible-pattern', - value: defaultProps.state.indexPatterns['3'], - }, - ]); - }); + const instance = shallow(); + selectIndexPatternPickerOption(instance, 'my-compatible-pattern'); expect(updateLayerIndexPattern).toHaveBeenCalledWith( defaultProps.state.layers.first, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.tsx index c221371dd6aa2..0fa2fb487b9c4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.tsx @@ -5,140 +5,39 @@ */ import _ from 'lodash'; -import React, { useState } from 'react'; -import { - EuiComboBox, - // @ts-ignore - EuiHighlight, - EuiButtonEmpty, - EuiIcon, - EuiIconTip, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { DatasourceLayerPanelProps, StateSetter } from '../types'; -import { IndexPatternPrivateState, IndexPatternLayer } from './indexpattern'; -import { isLayerTransferable, updateLayerIndexPattern } from './state_helpers'; +import { IndexPatternPrivateState } from './indexpattern'; +import { updateLayerIndexPattern } from './state_helpers'; +import { ChangeIndexPattern } from './change_indexpattern'; export interface IndexPatternLayerPanelProps extends DatasourceLayerPanelProps { state: IndexPatternPrivateState; setState: StateSetter; } - -function LayerPanelChooser({ - indexPatterns, - layer, - onChangeIndexPattern, - onExitChooser, -}: { - indexPatterns: IndexPatternPrivateState['indexPatterns']; - layer: IndexPatternLayer; - onChangeIndexPattern: (newId: string) => void; - onExitChooser: () => void; -}) { - const currentIndexPatternId = layer.indexPatternId; - const indexPatternList = Object.values(indexPatterns) - .filter(indexPattern => indexPattern.id !== layer.indexPatternId) - .map(indexPattern => ({ - ...indexPattern, - isTransferable: isLayerTransferable(layer, indexPattern), - })); - return ( - ({ - label: indexPattern.title, - value: indexPattern, - }))} - inputRef={el => { - if (el) { - el.focus(); - } - }} - selectedOptions={[ - { - label: indexPatterns[currentIndexPatternId].title, - value: indexPatterns[currentIndexPatternId].id, - }, - ]} - singleSelection={{ asPlainText: true }} - isClearable={false} - onBlur={onExitChooser} - onChange={choices => { - onChangeIndexPattern(choices[0].value!.id); - }} - renderOption={(option, searchValue, contentClassName) => { - const { label, value } = option; - return ( - - {value && value.isTransferable ? ( - - ) : ( - - )} - {label} - - ); - }} - /> - ); -} - export function LayerPanel({ state, setState, layerId }: IndexPatternLayerPanelProps) { - const [isChooserOpen, setChooserOpen] = useState(false); - return ( - - {isChooserOpen ? ( - - { - setChooserOpen(false); - }} - onChangeIndexPattern={newId => { - setState({ - ...state, - currentIndexPatternId: newId, - layers: { - ...state.layers, - [layerId]: updateLayerIndexPattern( - state.layers[layerId], - state.indexPatterns[newId] - ), - }, - }); - - setChooserOpen(false); - }} - /> - - ) : ( - - setChooserOpen(true)} - data-test-subj="lns_layerIndexPatternLabel" - > - {state.indexPatterns[state.layers[layerId].indexPatternId].title} - - - )} - + { + setState({ + ...state, + currentIndexPatternId: newId, + layers: { + ...state.layers, + [layerId]: updateLayerIndexPattern(state.layers[layerId], state.indexPatterns[newId]), + }, + }); + }} + /> ); } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 4d0c7b7044163..7170a41a16880 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -169,7 +169,7 @@ export function XYConfigPanel(props: VisualizationProps) { data-test-subj={`lnsXY_layer_${layer.layerId}`} paddingSize="s" > - + ) { /> - + ) { - +