diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx index a10f53a4895a9..16803dfda31bb 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, memo, useContext } from 'react'; -import { EuiSelect } from '@elastic/eui'; +import React, { useMemo, memo, useContext, useState } from 'react'; +import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; import { DatasourceDataPanelProps, Datasource } from '../../../public'; import { NativeRenderer } from '../../native_renderer'; import { Action } from './state_management'; @@ -36,21 +36,52 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { setState: setDatasourceState, }; + const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false); + return ( <> - ({ - value: datasourceId, - text: datasourceId, - }))} - value={props.activeDatasource || undefined} - onChange={e => { - props.dispatch({ type: 'SWITCH_DATASOURCE', newDatasourceId: e.target.value }); - }} - /> + {Object.keys(props.datasourceMap).length > 1 && ( + setDatasourceSwitcher(true)} + iconType="gear" + /> + } + isOpen={showDatasourceSwitcher} + closePopover={() => setDatasourceSwitcher(false)} + panelPaddingSize="none" + anchorPosition="rightUp" + > + ( + { + setDatasourceSwitcher(false); + props.dispatch({ + type: 'SWITCH_DATASOURCE', + newDatasourceId: datasourceId, + }); + }} + > + {datasourceId} + + ))} + /> + + )} {props.activeDatasource && !props.datasourceIsLoading && ( diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index 4bcf4d8b5e42e..7326c0de7723e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx @@ -502,6 +502,10 @@ Object { instance.update(); }); + afterEach(() => { + instance.unmount(); + }); + it('should have initialized only the initial datasource and visualization', () => { expect(mockDatasource.initialize).toHaveBeenCalled(); expect(mockDatasource2.initialize).not.toHaveBeenCalled(); @@ -512,9 +516,12 @@ Object { it('should initialize other datasource on switch', async () => { act(() => { - instance - .find('select[data-test-subj="datasource-switch"]') - .simulate('change', { target: { value: 'testDatasource2' } }); + instance.find('button[data-test-subj="datasource-switch"]').simulate('click'); + }); + act(() => { + (document.querySelector( + '[data-test-subj="datasource-switch-testDatasource2"]' + ) as HTMLButtonElement).click(); }); expect(mockDatasource2.initialize).toHaveBeenCalled(); }); @@ -523,9 +530,11 @@ Object { const initialState = {}; mockDatasource2.initialize.mockResolvedValue(initialState); - instance - .find('select[data-test-subj="datasource-switch"]') - .simulate('change', { target: { value: 'testDatasource2' } }); + instance.find('button[data-test-subj="datasource-switch"]').simulate('click'); + + (document.querySelector( + '[data-test-subj="datasource-switch-testDatasource2"]' + ) as HTMLButtonElement).click(); await waitForPromises(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss index b57fe73adb8b2..8b50405ae5f3b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss @@ -20,19 +20,31 @@ } .lnsSidebar { - @include euiScrollBar; - overflow: hidden auto; - padding: $euiSize; margin: 0; flex: 1 0 18%; - min-width: ($euiSize * 16); + min-width: ($euiSize * 22); height: 100%; display: flex; flex-direction: column; + position: relative; } .lnsSidebar--right { - min-width: ($euiSize * 18); + min-width: ($euiSize * 22); + @include euiScrollBar; + overflow: hidden auto; + padding: $euiSize; +} + +.lnsSidebarContainer { + flex: 1 0 100%; + overflow: hidden; +} + +.lnsDatasourceSwitch { + position: absolute; + right: $euiSize + $euiSizeXS; + top: $euiSize + $euiSizeXS; } .lnsPageBody { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__snapshots__/indexpattern.test.tsx.snap b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__snapshots__/indexpattern.test.tsx.snap deleted file mode 100644 index 5b260ec1b7458..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__snapshots__/indexpattern.test.tsx.snap +++ /dev/null @@ -1,87 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`IndexPattern Data Source #renderDataPanel should match snapshot 1`] = ` - - Index Pattern Data Source -
- -
- - timestamp - - - bytes - - - source - -
-
-
-`; 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 new file mode 100644 index 0000000000000..7d219f0d5808f --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx @@ -0,0 +1,269 @@ +/* + * 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 { shallow } from 'enzyme'; +import React, { ChangeEvent, ReactElement } from 'react'; +import { EuiComboBox, EuiFieldSearch, EuiContextMenuPanel } from '@elastic/eui'; +import { IndexPatternPrivateState } from './indexpattern'; +import { DatasourceDataPanelProps } from '../types'; +import { createMockedDragDropContext } from './mocks'; +import { IndexPatternDataPanel } from './datapanel'; +import { FieldItem } from './field_item'; +import { act } from 'react-dom/test-utils'; + +jest.mock('./loader'); + +const initialState: IndexPatternPrivateState = { + currentIndexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'My Op', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + sourceField: 'op', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + }, + }, + }, + indexPatterns: { + '1': { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + fields: [ + { + name: 'timestamp', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'unsupported', + type: 'geo', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + ], + }, + '2': { + id: '2', + title: 'my-fake-restricted-pattern', + timeFieldName: 'timestamp', + fields: [ + { + name: 'timestamp', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', + }, + }, + }, + { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + histogram: { + agg: 'histogram', + interval: 1000, + }, + avg: { + agg: 'avg', + }, + max: { + agg: 'max', + }, + min: { + agg: 'min', + }, + sum: { + agg: 'sum', + }, + }, + }, + { + name: 'source', + type: 'string', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + terms: { + agg: 'terms', + }, + }, + }, + ], + }, + }, +}; +describe('IndexPattern Data Panel', () => { + let defaultProps: DatasourceDataPanelProps; + + beforeEach(() => { + defaultProps = { + state: initialState, + setState: jest.fn(), + dragDropContext: createMockedDragDropContext(), + }; + }); + + it('should render a warning if there are no index patterns', () => { + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="indexPattern-no-indexpatterns"]')).toHaveLength(1); + }); + + it('should call setState when the index pattern is switched', async () => { + const wrapper = shallow(); + + wrapper.find('[data-test-subj="indexPattern-switch-link"]').simulate('click'); + + const comboBox = wrapper.find(EuiComboBox); + + comboBox.prop('onChange')!([ + { + label: initialState.indexPatterns['2'].title, + value: '2', + }, + ]); + + expect(defaultProps.setState).toHaveBeenCalledWith({ + ...initialState, + currentIndexPatternId: '2', + }); + }); + + it('should list all supported fields in the pattern sorted alphabetically', async () => { + const wrapper = shallow(); + + expect(wrapper.find(FieldItem).map(fieldItem => fieldItem.prop('field').name)).toEqual([ + 'bytes', + 'memory', + 'source', + 'timestamp', + ]); + }); + + it('should filter down by name', async () => { + const wrapper = shallow(); + + act(() => { + wrapper.find(EuiFieldSearch).prop('onChange')!({ target: { value: 'mem' } } as ChangeEvent< + HTMLInputElement + >); + }); + + expect(wrapper.find(FieldItem).map(fieldItem => fieldItem.prop('field').name)).toEqual([ + 'memory', + ]); + }); + + it('should filter down by type', async () => { + const wrapper = shallow(); + + act(() => { + (wrapper + .find(EuiContextMenuPanel) + .prop('items')! + .find( + item => (item as ReactElement).props['data-test-subj'] === 'typeFilter-number' + )! as ReactElement).props.onClick(); + }); + + expect(wrapper.find(FieldItem).map(fieldItem => fieldItem.prop('field').name)).toEqual([ + 'bytes', + 'memory', + ]); + }); + + it('should toggle type if clicked again', async () => { + const wrapper = shallow(); + + act(() => { + (wrapper + .find(EuiContextMenuPanel) + .prop('items')! + .find( + item => (item as ReactElement).props['data-test-subj'] === 'typeFilter-number' + )! as ReactElement).props.onClick(); + }); + + act(() => { + (wrapper + .find(EuiContextMenuPanel) + .prop('items')! + .find( + item => (item as ReactElement).props['data-test-subj'] === 'typeFilter-number' + )! as ReactElement).props.onClick(); + }); + + expect(wrapper.find(FieldItem).map(fieldItem => fieldItem.prop('field').name)).toEqual([ + 'bytes', + 'memory', + 'source', + 'timestamp', + ]); + }); + + it('should filter down by type and by name', async () => { + const wrapper = shallow(); + + act(() => { + wrapper.find(EuiFieldSearch).prop('onChange')!({ target: { value: 'mem' } } as ChangeEvent< + HTMLInputElement + >); + }); + + act(() => { + (wrapper + .find(EuiContextMenuPanel) + .prop('items')! + .find( + item => (item as ReactElement).props['data-test-subj'] === 'typeFilter-number' + )! as ReactElement).props.onClick(); + }); + + expect(wrapper.find(FieldItem).map(fieldItem => fieldItem.prop('field').name)).toEqual([ + 'memory', + ]); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx new file mode 100644 index 0000000000000..791f071f57b9d --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.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 _ from 'lodash'; +import React, { useState } from 'react'; +import { + EuiComboBox, + EuiFieldSearch, + // @ts-ignore + EuiHighlight, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiButtonEmpty, + EuiFilterGroup, + EuiFilterButton, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiContextMenuPanelProps, + EuiPopover, + EuiCallOut, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DatasourceDataPanelProps, DataType } from '../types'; +import { IndexPatternPrivateState, IndexPatternField } from './indexpattern'; +import { ChildDragDropProvider } from '../drag_drop'; +import { FieldItem } from './field_item'; +import { FieldIcon } from './field_icon'; + +// TODO the typings for EuiContextMenuPanel are incorrect - watchedItemProps is missing. This can be removed when the types are adjusted +const FixedEuiContextMenuPanel = (EuiContextMenuPanel as any) as React.FunctionComponent< + EuiContextMenuPanelProps & { watchedItemProps: string[] } +>; + +function sortFields(fieldA: IndexPatternField, fieldB: IndexPatternField) { + return fieldA.name.toLowerCase() < fieldB.name.toLowerCase() ? -1 : 1; +} + +const supportedFieldTypes = ['string', 'number', 'boolean', 'date']; + +const fieldTypeNames: Record = { + string: i18n.translate('xpack.lens.datatypes.string', { defaultMessage: 'string' }), + number: i18n.translate('xpack.lens.datatypes.number', { defaultMessage: 'number' }), + boolean: i18n.translate('xpack.lens.datatypes.boolean', { defaultMessage: 'boolean' }), + date: i18n.translate('xpack.lens.datatypes.date', { defaultMessage: 'date' }), +}; + +export function IndexPatternDataPanel(props: DatasourceDataPanelProps) { + const [nameFilter, setNameFilter] = useState(''); + const [typeFilter, setTypeFilter] = useState([]); + + const [showIndexPatternSwitcher, setShowIndexPatternSwitcher] = useState(false); + const [isTypeFilterOpen, setTypeFilterOpen] = useState(false); + + if (Object.keys(props.state.indexPatterns).length === 0) { + return ( + + + +

+ +

+
+
+
+ ); + } + + const filteredFields = props.state.indexPatterns[props.state.currentIndexPatternId].fields.filter( + (field: IndexPatternField) => + field.name.toLowerCase().includes(nameFilter.toLowerCase()) && + supportedFieldTypes.includes(field.type) + ); + + const availableFieldTypes = _.uniq(filteredFields.map(({ type }) => type)); + const availableFilteredTypes = typeFilter.filter(type => availableFieldTypes.includes(type)); + + return ( + + + +
+ {!showIndexPatternSwitcher ? ( + <> + +

+ {props.state.indexPatterns[props.state.currentIndexPatternId].title}{' '} +

+
+ setShowIndexPatternSwitcher(true)} + size="xs" + > + ( + + ) + + + ) : ( + ({ + label: title, + value: id, + }))} + inputRef={el => { + if (el) { + el.focus(); + } + }} + selectedOptions={ + props.state.currentIndexPatternId + ? [ + { + label: props.state.indexPatterns[props.state.currentIndexPatternId].title, + value: props.state.indexPatterns[props.state.currentIndexPatternId].id, + }, + ] + : undefined + } + singleSelection={{ asPlainText: true }} + isClearable={false} + onBlur={() => { + setShowIndexPatternSwitcher(false); + }} + onChange={choices => { + setShowIndexPatternSwitcher(false); + props.setState({ + ...props.state, + currentIndexPatternId: choices[0].value as string, + }); + }} + /> + )} +
+
+ + + + { + setNameFilter(e.target.value); + }} + aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', { + defaultMessage: 'Search fields', + })} + /> + + + + setTypeFilterOpen(false)} + button={ + setTypeFilterOpen(!isTypeFilterOpen)} + iconType="arrowDown" + data-test-subj="indexPatternTypeFilterButton" + isSelected={isTypeFilterOpen} + numFilters={availableFieldTypes.length} + hasActiveFilters={availableFilteredTypes.length > 0} + numActiveFilters={availableFilteredTypes.length} + > + {i18n.translate('xpack.lens.indexPatterns.typeFilterLabel', { + defaultMessage: 'Types', + })} + + } + > + ( + { + setTypeFilter( + typeFilter.includes(type) + ? typeFilter.filter(t => t !== type) + : [...typeFilter, type] + ); + }} + > + {fieldTypeNames[type]} + + ))} + /> + + + + +
+
+ {filteredFields + .filter( + field => typeFilter.length === 0 || typeFilter.includes(field.type as DataType) + ) + .sort(sortFields) + .map(field => ( + + ))} +
+
+
+
+
+ ); +} 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 9cb3232aeb295..ccae065c404f3 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 @@ -7,8 +7,12 @@ import _ from 'lodash'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiComboBox } from '@elastic/eui'; +import { EuiComboBox, EuiFlexGroup, EuiFlexItem, EuiComboBoxOptionProps } from '@elastic/eui'; import classNames from 'classnames'; +import { + // @ts-ignore + EuiHighlight, +} from '@elastic/eui'; import { IndexPatternColumn, FieldBasedIndexPatternColumn, @@ -16,6 +20,8 @@ import { BaseIndexPatternColumn, } from '../indexpattern'; import { hasField, sortByField } from '../state_helpers'; +import { FieldIcon } from '../field_icon'; +import { DataType } from '../../types'; export interface FieldSelectProps { incompatibleSelectedOperationType: OperationType | null; @@ -60,7 +66,7 @@ export function FieldSelect({ label: i18n.translate('xpack.lens.indexPattern.documentField', { defaultMessage: 'Document', }), - value: fieldLessColumn.operationId, + value: { operationId: fieldLessColumn.operationId, dataType: fieldLessColumn.dataType }, className: classNames({ 'lnsConfigPanel__fieldOption--incompatible': !isCompatibleWithCurrentOperation( fieldLessColumn @@ -77,7 +83,7 @@ export function FieldSelect({ options: uniqueColumnsByField .map(col => ({ label: col.sourceField, - value: col.operationId, + value: { operationId: col.operationId, dataType: col.dataType }, compatible: isCompatibleWithCurrentOperation(col), })) .sort(({ compatible: a }, { compatible: b }) => { @@ -104,7 +110,7 @@ export function FieldSelect({ placeholder={i18n.translate('xpack.lens.indexPattern.fieldPlaceholder', { defaultMessage: 'Field', })} - options={fieldOptions} + options={(fieldOptions as unknown) as EuiComboBoxOptionProps[]} isInvalid={Boolean(incompatibleSelectedOperationType && selectedColumn)} selectedOptions={ selectedColumn @@ -126,11 +132,24 @@ export function FieldSelect({ } const column: IndexPatternColumn = filteredColumns.find( - ({ operationId }) => operationId === choices[0].value + ({ operationId }) => + operationId === ((choices[0].value as unknown) as { operationId: string }).operationId )!; onChangeColumn(column); }} + renderOption={(option, searchValue) => { + return ( + + + + + + {option.label} + + + ); + }} /> ); } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx new file mode 100644 index 0000000000000..8b6ef000a15bf --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx @@ -0,0 +1,35 @@ +/* + * 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 React from 'react'; +import { ICON_TYPES, palettes, EuiIcon } from '@elastic/eui'; +import classNames from 'classnames'; +import { DataType } from '../types'; + +function stringToNum(s: string) { + // tslint:disable-next-line:no-bitwise + return Array.from(s).reduce((acc, ch) => acc + ch.charCodeAt(0), 1); +} + +export type UnwrapArray = T extends Array ? P : T; + +export function FieldIcon({ type }: { type: DataType }) { + const icons: Partial>> = { + boolean: 'invert', + date: 'calendar', + }; + + const iconType = icons[type] || ICON_TYPES.find(t => t === type) || 'empty'; + const { colors } = palettes.euiPaletteColorBlind; + const colorIndex = stringToNum(iconType) % colors.length; + + const classes = classNames( + 'lnsFieldListPanel__fieldIcon', + `lnsFieldListPanel__fieldIcon--${type}` + ); + + return ; +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx new file mode 100644 index 0000000000000..c5371bfec6214 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx @@ -0,0 +1,70 @@ +/* + * 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 React from 'react'; +import { + // @ts-ignore + EuiHighlight, +} from '@elastic/eui'; +import { IndexPatternField } from './indexpattern'; +import { DragDrop } from '../drag_drop'; +import { FieldIcon } from './field_icon'; +import { DataType } from '../types'; + +export interface FieldItemProps { + field: IndexPatternField; + highlight?: string; +} + +function highglightedPart(completeHighlight: string | undefined, part: string) { + if (!completeHighlight) { + return ''; + } + + if (completeHighlight.includes(part)) { + return part; + } + + const highlightParts = completeHighlight.split('.').filter(highlightPart => highlightPart !== ''); + + const lastHighlightPart = highlightParts[highlightParts.length - 1]; + if (part.startsWith(lastHighlightPart)) { + return lastHighlightPart; + } + + const firstHighlightPart = highlightParts[0]; + if (part.endsWith(firstHighlightPart)) { + return firstHighlightPart; + } + + return completeHighlight; +} + +export function FieldItem({ field, highlight }: FieldItemProps) { + const fieldParts = field.name.split('.'); + const wrappableHighlightableFieldName = _.flatten( + fieldParts.map((part, index) => [ + + {part} + {index !== fieldParts.length - 1 ? '.' : ''} + , + index !== fieldParts.length - 1 ? : null, + ]) + ); + + return ( + + + + {wrappableHighlightableFieldName} + + + ); +} 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 877afd3fcbbc4..47415a82beef7 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.scss @@ -1,5 +1,61 @@ @import './dimension_panel/index'; -.lnsIndexPattern__dimensionPopover { - max-width: 600px; +.lnsIndexPatternDataPanel__header { + display: flex; + align-items: center; + height: $euiSize * 3; + margin-top: -$euiSizeS; + + + & > .lnsIndexPatternDataPanel__changeLink { + flex: 0 0 auto; + margin: 0 $euiSize; + } + + & > h4 { + flex: 0 1 auto; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.lnsIndexPatternDataPanel { + width: 100%; + height: 100%; + padding: $euiSize $euiSize 0; +} + +.lnsFieldListPanel__list { + @include euiOverflowShadow($euiPageBackgroundColor); + height: 100%; + margin-top: 2px; // form control shadow +} + +.lnsFieldListPanel__overflow { + padding-top: $euiSizeS; + height: 100%; + overflow: visible auto; + @include euiScrollBar; +} + +.lnsFieldListPanel__field { + @include euiFontSizeS; + background: $euiColorEmptyShade; + border-radius: $euiBorderRadius; + padding: $euiSizeS; + display: flex; + align-items: center; + margin-bottom: $euiSizeXS; + font-weight: $euiFontWeightMedium; + transition: box-shadow $euiAnimSpeedFast $euiAnimSlightResistance; + + &:hover { + @include euiBottomShadowMedium; + z-index: 2; + cursor: grab; + } +} + +.lnsFieldListPanel__fieldName { + margin-left: $euiSizeXS; } 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 c3610ab2cf95d..e853b63de495e 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 @@ -4,17 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; -import React from 'react'; -import { EuiComboBox } from '@elastic/eui'; import { getIndexPatternDatasource, IndexPatternPersistedState, IndexPatternPrivateState, - IndexPatternDataPanel, } from './indexpattern'; import { DatasourcePublicAPI, Operation, Datasource } from '../types'; -import { createMockedDragDropContext } from './mocks'; jest.mock('./loader'); @@ -153,51 +148,6 @@ describe('IndexPattern Data Source', () => { }); }); - describe('#renderDataPanel', () => { - let state: IndexPatternPrivateState; - - beforeEach(async () => { - state = await indexPatternDatasource.initialize(persistedState); - }); - - it('should match snapshot', () => { - expect( - shallow( - {}} - /> - ) - ).toMatchSnapshot(); - }); - - it('should call setState when the index pattern is switched', async () => { - const setState = jest.fn(); - - const wrapper = shallow( - - ); - - const comboBox = wrapper.find(EuiComboBox); - - comboBox.prop('onChange')!([ - { - label: expectedIndexPatterns['2'].title, - value: '2', - }, - ]); - - expect(setState).toHaveBeenCalledWith({ - ...state, - currentIndexPatternId: '2', - }); - }); - }); - describe('#getPersistedState', () => { it('should persist from saved state', async () => { const state = await indexPatternDatasource.initialize(persistedState); 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 a1b3d9b6efe3d..4504ac41d6dde 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { render } from 'react-dom'; import { Chrome } from 'ui/chrome'; import { ToastNotifications } from 'ui/notify/toasts/toast_notifications'; -import { EuiComboBox } from '@elastic/eui'; import uuid from 'uuid'; import { Datasource, DataType } from '..'; +import { I18nProvider } from '@kbn/i18n/react'; import { DatasourceDimensionPanelProps, DatasourceDataPanelProps, @@ -19,10 +19,10 @@ import { DatasourceSuggestion, } from '../types'; import { getIndexPatterns } from './loader'; -import { ChildDragDropProvider, DragDrop } from '../drag_drop'; import { toExpression } from './to_expression'; import { IndexPatternDimensionPanel } from './dimension_panel'; import { buildColumnForOperationType, getOperationTypesForField } from './operations'; +import { IndexPatternDataPanel } from './datapanel'; export type OperationType = IndexPatternColumn['operationType']; @@ -122,49 +122,6 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & { indexPatterns: Record; }; -export function IndexPatternDataPanel(props: DatasourceDataPanelProps) { - return ( - - Index Pattern Data Source -
- ({ - label: title, - value: id, - }))} - selectedOptions={ - props.state.currentIndexPatternId - ? [ - { - label: props.state.indexPatterns[props.state.currentIndexPatternId].title, - value: props.state.indexPatterns[props.state.currentIndexPatternId].id, - }, - ] - : undefined - } - singleSelection={{ asPlainText: true }} - isClearable={false} - onChange={choices => { - props.setState({ - ...props.state, - currentIndexPatternId: choices[0].value as string, - }); - }} - /> -
- {props.state.currentIndexPatternId && - props.state.indexPatterns[props.state.currentIndexPatternId].fields.map(field => ( - - {field.name} - - ))} -
-
-
- ); -} - export function columnToOperation(column: IndexPatternColumn) { const { dataType, label, isBucketed, operationId } = column; return { @@ -248,7 +205,12 @@ export function getIndexPatternDatasource(chrome: Chrome, toastNotifications: To domElement: Element, props: DatasourceDataPanelProps ) { - render(, domElement); + render( + + + , + domElement + ); }, getPublicAPI(state, setState) {