From 83d9ef0f2d00175915f2c878d45fabb2e4289d90 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 30 Dec 2020 11:01:54 -0500 Subject: [PATCH 1/3] [Lens] Fix duplicate suggestions on single-bucket charts (#86996) --- .../indexpattern_suggestions.test.tsx | 17 ++++++++++++++++- .../indexpattern_suggestions.ts | 9 ++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index de768e92efb3d..97a63de4f7ba2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -1876,7 +1876,7 @@ describe('IndexPattern Data Source suggestions', () => { expect(suggestions.length).toBe(6); }); - it('returns an only metric version of a given table', () => { + it('returns an only metric version of a given table, but does not include current state as reduced', () => { const initialState = testInitialState(); const state: IndexPatternPrivateState = { indexPatternRefs: [], @@ -1953,6 +1953,21 @@ describe('IndexPattern Data Source suggestions', () => { }; const suggestions = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); + expect(suggestions).not.toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + changeType: 'reduced', + columns: [ + expect.objectContaining({ + operation: expect.objectContaining({ label: 'field2' }), + }), + expect.objectContaining({ + operation: expect.objectContaining({ label: 'Average of field1' }), + }), + ], + }), + }) + ); expect(suggestions).toContainEqual( expect.objectContaining({ table: expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 969324c67e909..9d7328b4dca37 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -583,8 +583,9 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer columnOrder: [...bucketedColumns, ...availableMetricColumns], }; - if (availableReferenceColumns.length) { - // Don't remove buckets when dealing with any refs. This can break refs. + if (availableBucketedColumns.length <= 1 || availableReferenceColumns.length) { + // Don't simplify when dealing with single-bucket table. Also don't break + // reference-based columns by removing buckets. return []; } else if (availableMetricColumns.length > 1) { return [{ ...layer, columnOrder: [...bucketedColumns, availableMetricColumns[0]] }]; @@ -597,7 +598,6 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer availableReferenceColumns.length ? [] : availableMetricColumns.map((columnId) => { - // build suggestions with only metrics return { ...layer, columnOrder: [columnId] }; }) ) @@ -606,8 +606,7 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer state, layerId, updatedLayer, - changeType: - layer.columnOrder.length === updatedLayer.columnOrder.length ? 'unchanged' : 'reduced', + changeType: 'reduced', label: updatedLayer.columnOrder.length === 1 ? getMetricSuggestionTitle(updatedLayer, availableMetricColumns.length === 1) From 0c9a573515484dde313a273722f4caade021a27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Wed, 30 Dec 2020 18:05:31 +0100 Subject: [PATCH 2/3] [Security Solution] Fix Timeline filter EuiSuperSelect styling (#87033) --- .../search_or_filter/search_or_filter.tsx | 7 +- .../search_or_filter/super_select.tsx | 273 ++++++++++++++++++ 2 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/super_select.tsx diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 1b5e6932dff08..7ae8cf4c6507c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import React, { useCallback } from 'react'; import styled, { createGlobalStyle } from 'styled-components'; @@ -15,6 +15,7 @@ import { DispatchUpdateReduxTime } from '../../../../common/components/super_dat import { DataProvider } from '../data_providers/data_provider'; import { QueryBarTimeline } from '../query_bar'; +import { EuiSuperSelect } from './super_select'; import { options } from './helpers'; import * as i18n from './translations'; @@ -28,8 +29,8 @@ const SearchOrFilterGlobalStyle = createGlobalStyle` width: 350px !important; } - .${searchOrFilterPopoverClassName}__popoverPanel { - width: ${searchOrFilterPopoverWidth}; + .${searchOrFilterPopoverClassName}.euiPopover__panel { + width: ${searchOrFilterPopoverWidth} !important; .euiSuperSelect__listbox { width: ${searchOrFilterPopoverWidth} !important; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/super_select.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/super_select.tsx new file mode 100644 index 0000000000000..36a6ea7385b8a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/super_select.tsx @@ -0,0 +1,273 @@ +/* + * 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. + */ + +/* + Duplicated EuiSuperSelect, because due to the recent changes there is no way to pass panelClassName + prop to EuiInputPopover, which doesn't allow us to properly style the EuiInputPopover panel + (we want the panel to be wider than the input) +*/ + +import { + EuiSuperSelectProps, + EuiScreenReaderOnly, + EuiSuperSelectControl, + EuiInputPopover, + EuiContextMenuItem, + keys, + EuiI18n, +} from '@elastic/eui'; +import React, { Component } from 'react'; +import classNames from 'classnames'; + +enum ShiftDirection { + BACK = 'back', + FORWARD = 'forward', +} + +export class EuiSuperSelect extends Component> { + static defaultProps = { + compressed: false, + fullWidth: false, + hasDividers: false, + isInvalid: false, + isLoading: false, + }; + + private itemNodes: Array = []; + private _isMounted: boolean = false; + + state = { + isPopoverOpen: this.props.isOpen || false, + }; + + componentDidMount() { + this._isMounted = true; + if (this.props.isOpen) { + this.openPopover(); + } + } + + componentWillUnmount() { + this._isMounted = false; + } + + setItemNode = (node: HTMLButtonElement | null, index: number) => { + this.itemNodes[index] = node; + }; + + openPopover = () => { + this.setState({ + isPopoverOpen: true, + }); + + const focusSelected = () => { + const indexOfSelected = this.props.options.reduce((acc, option, index) => { + if (acc != null) return acc; + if (option == null) return null; + return option.value === this.props.valueOfSelected ? index : null; + }, null); + + requestAnimationFrame(() => { + if (!this._isMounted) { + return; + } + + if (this.props.valueOfSelected != null) { + if (indexOfSelected != null) { + this.focusItemAt(indexOfSelected); + } else { + focusSelected(); + } + } + }); + }; + + requestAnimationFrame(focusSelected); + }; + + closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; + + itemClicked = (value: T) => { + this.setState({ + isPopoverOpen: false, + }); + if (this.props.onChange) { + this.props.onChange(value); + } + }; + + onSelectKeyDown = (event: React.KeyboardEvent) => { + if (event.key === keys.ARROW_UP || event.key === keys.ARROW_DOWN) { + event.preventDefault(); + event.stopPropagation(); + this.openPopover(); + } + }; + + onItemKeyDown = (event: React.KeyboardEvent) => { + switch (event.key) { + case keys.ESCAPE: + // close the popover and prevent ancestors from handling + event.preventDefault(); + event.stopPropagation(); + this.closePopover(); + break; + + case keys.TAB: + // no-op + event.preventDefault(); + event.stopPropagation(); + break; + + case keys.ARROW_UP: + event.preventDefault(); + event.stopPropagation(); + this.shiftFocus(ShiftDirection.BACK); + break; + + case keys.ARROW_DOWN: + event.preventDefault(); + event.stopPropagation(); + this.shiftFocus(ShiftDirection.FORWARD); + break; + } + }; + + focusItemAt(index: number) { + const targetElement = this.itemNodes[index]; + if (targetElement != null) { + targetElement.focus(); + } + } + + shiftFocus(direction: ShiftDirection) { + const currentIndex = this.itemNodes.indexOf(document.activeElement as HTMLButtonElement); + let targetElementIndex: number; + + if (currentIndex === -1) { + // somehow the select options has lost focus + targetElementIndex = 0; + } else { + if (direction === ShiftDirection.BACK) { + targetElementIndex = currentIndex === 0 ? this.itemNodes.length - 1 : currentIndex - 1; + } else { + targetElementIndex = currentIndex === this.itemNodes.length - 1 ? 0 : currentIndex + 1; + } + } + + this.focusItemAt(targetElementIndex); + } + + render() { + const { + className, + options, + valueOfSelected, + onChange, + isOpen, + isInvalid, + hasDividers, + itemClassName, + itemLayoutAlign, + fullWidth, + popoverClassName, + compressed, + ...rest + } = this.props; + + const popoverClasses = classNames('euiSuperSelect', popoverClassName); + + const buttonClasses = classNames( + { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'euiSuperSelect--isOpen__button': this.state.isPopoverOpen, + }, + className + ); + + const itemClasses = classNames( + 'euiSuperSelect__item', + { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'euiSuperSelect__item--hasDividers': hasDividers, + }, + itemClassName + ); + + const button = ( + + ); + + const items = options.map((option, index) => { + const { value, dropdownDisplay, inputDisplay, ...optionRest } = option; + + return ( + this.itemClicked(value)} + onKeyDown={this.onItemKeyDown} + layoutAlign={itemLayoutAlign} + buttonRef={(node) => this.setItemNode(node, index)} + role="option" + id={value} + aria-selected={valueOfSelected === value} + {...optionRest} + > + {dropdownDisplay || inputDisplay} + + ); + }); + + return ( + + +

+ +

+
+
+ {items} +
+
+ ); + } +} From 71f74314ce8b20b54104018a5a7716e9a5497226 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 30 Dec 2020 10:32:48 -0700 Subject: [PATCH 3/3] [test/functional_cors] 9000 is sometimes in use, make getPort random (#87050) Co-authored-by: spalger --- x-pack/test/functional_cors/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional_cors/config.ts b/x-pack/test/functional_cors/config.ts index b792aa2d183b6..737cccc5ae33b 100644 --- a/x-pack/test/functional_cors/config.ts +++ b/x-pack/test/functional_cors/config.ts @@ -27,7 +27,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }; const { protocol, hostname } = kbnTestConfig.getUrlParts(); - const pluginPort = await getPort({ port: 9000 }); + const pluginPort = await getPort(); const originUrl = Url.format({ protocol, hostname,