From 362ef647511a7d69d1c4d2d6d8cf6246d0c28e9a Mon Sep 17 00:00:00 2001 From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:36:08 +0100 Subject: [PATCH] [Lens] [Unified Search] Fix field truncation on Combo boxes (#170889) ## Summary Fixes 3/5 of https://github.com/elastic/kibana/issues/168753 Doesn't set up multilines. Doesn't remove auto-expanding logic. Middle truncates. (Unified Search) Field selector Screenshot 2023-11-13 at 11 30 20 (Unified Search) Value selector: Screenshot 2023-11-13 at 11 30 30 (Lens) Field picker within layer config: Screenshot 2023-11-09 at 14 44 58 Also fixes tiny stylistic issue for dataview picker label cut on the bottom: Screenshot 2023-11-09 at 15 06 38 Screenshot 2023-11-09 at 15 07 08 --- .../components/field_picker/field_picker.tsx | 149 ++++++++---------- .../components/field_picker/index.ts | 1 - .../field_picker/truncated_label.test.tsx | 78 --------- .../field_picker/truncated_label.tsx | 86 ---------- .../kbn-visualization-ui-components/index.ts | 1 - .../public/filter_badge/filter_badge.tsx | 1 + .../filter_editor/filter_editor.tsx | 11 +- .../filter_editor/generic_combo_box.tsx | 3 +- .../public/filter_bar/filter_editor/index.ts | 1 - .../filter_bar/filter_editor/lib/helpers.ts | 3 + .../filter_editor/phrase_value_input.tsx | 24 +-- .../filter_editor/phrases_values_input.tsx | 24 +-- .../filter_editor/truncated_label.test.tsx | 79 ---------- .../filter_editor/truncated_label.tsx | 141 ----------------- .../filter_bar/filter_item/filter_item.tsx | 2 +- .../filter_item/field_input.tsx | 38 +---- .../dataview_picker/trigger.tsx | 2 +- 17 files changed, 98 insertions(+), 546 deletions(-) delete mode 100644 packages/kbn-visualization-ui-components/components/field_picker/truncated_label.test.tsx delete mode 100644 packages/kbn-visualization-ui-components/components/field_picker/truncated_label.tsx delete mode 100644 src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.test.tsx delete mode 100644 src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.tsx diff --git a/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx index 4827bb2fd628..5b6022d5cb45 100644 --- a/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx +++ b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx @@ -7,14 +7,11 @@ */ import './field_picker.scss'; -import React, { useRef } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import useEffectOnce from 'react-use/lib/useEffectOnce'; -import { EuiComboBox, EuiComboBoxProps, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; import { FieldIcon } from '@kbn/field-utils/src/components/field_icon'; import classNames from 'classnames'; -import type { DataType } from './types'; -import { TruncatedLabel } from './truncated_label'; import type { FieldOptionValue, FieldOption } from './types'; export interface FieldPickerProps @@ -27,9 +24,8 @@ export interface FieldPickerProps 'data-test-subj'?: string; } -const DEFAULT_COMBOBOX_WIDTH = 305; -const COMBOBOX_PADDINGS = 90; -const DEFAULT_FONT = '14px Inter'; +const MIDDLE_TRUNCATION_PROPS = { truncation: 'middle' as const }; +const SINGLE_SELECTION_AS_TEXT_PROPS = { asPlainText: true }; export function FieldPicker({ selectedOptions, @@ -40,95 +36,86 @@ export function FieldPicker({ ['data-test-subj']: dataTestSub, ...rest }: FieldPickerProps) { + let theLongestLabel = ''; const styledOptions = options?.map(({ compatible, exists, ...otherAttr }) => { if (otherAttr.options) { return { ...otherAttr, - options: otherAttr.options.map(({ exists: fieldOptionExists, ...fieldOption }) => ({ - ...fieldOption, - className: classNames({ - 'lnFieldPicker__option--incompatible': !fieldOption.compatible, - 'lnFieldPicker__option--nonExistant': !fieldOptionExists, - }), - })), + options: otherAttr.options.map(({ exists: fieldOptionExists, ...fieldOption }) => { + if (fieldOption.label.length > theLongestLabel.length) { + theLongestLabel = fieldOption.label; + } + return { + ...fieldOption, + prepend: fieldOption.value.dataType ? ( + + ) : null, + className: classNames({ + 'lnFieldPicker__option--incompatible': !fieldOption.compatible, + 'lnFieldPicker__option--nonExistant': !fieldOptionExists, + }), + }; + }), }; } return { ...otherAttr, compatible, + prepend: otherAttr.value.dataType ? ( + + ) : null, className: classNames({ 'lnFieldPicker__option--incompatible': !compatible, 'lnFieldPicker__option--nonExistant': !exists, }), }; }); - const comboBoxRef = useRef(null); - const [labelProps, setLabelProps] = React.useState<{ - width: number; - font: string; - }>({ - width: DEFAULT_COMBOBOX_WIDTH - COMBOBOX_PADDINGS, - font: DEFAULT_FONT, - }); - - const computeStyles = (_e: UIEvent | undefined, shouldRecomputeAll = false) => { - if (comboBoxRef.current) { - const current = { - ...labelProps, - width: comboBoxRef.current?.clientWidth - COMBOBOX_PADDINGS, - }; - if (shouldRecomputeAll) { - current.font = window.getComputedStyle(comboBoxRef.current).font; - } - setLabelProps(current); - } - }; - - useEffectOnce(() => { - if (comboBoxRef.current) { - computeStyles(undefined, true); - } - window.addEventListener('resize', computeStyles); - }); + const panelMinWidth = getPanelMinWidth(theLongestLabel.length); return ( -
- { - if (choices.length === 0) { - onDelete?.(); - return; - } - onChoose(choices[0].value); - }} - renderOption={(option, searchValue) => { - return ( - - - - - - - - - ); - }} - {...rest} - /> -
+ { + if (choices.length === 0) { + onDelete?.(); + return; + } + onChoose(choices[0].value); + }} + {...rest} + /> ); } + +const MINIMUM_POPOVER_WIDTH = 300; +const MINIMUM_POPOVER_WIDTH_CHAR_COUNT = 28; +const AVERAGE_CHAR_WIDTH = 7; +const MAXIMUM_POPOVER_WIDTH_CHAR_COUNT = 60; +const MAXIMUM_POPOVER_WIDTH = 550; // fitting 60 characters + +function getPanelMinWidth(labelLength: number) { + if (labelLength > MAXIMUM_POPOVER_WIDTH_CHAR_COUNT) { + return MAXIMUM_POPOVER_WIDTH; + } + if (labelLength > MINIMUM_POPOVER_WIDTH_CHAR_COUNT) { + const overflownChars = labelLength - MINIMUM_POPOVER_WIDTH_CHAR_COUNT; + return MINIMUM_POPOVER_WIDTH + overflownChars * AVERAGE_CHAR_WIDTH; + } + return MINIMUM_POPOVER_WIDTH; +} diff --git a/packages/kbn-visualization-ui-components/components/field_picker/index.ts b/packages/kbn-visualization-ui-components/components/field_picker/index.ts index 96f53e2bfc74..e21e97911ecb 100644 --- a/packages/kbn-visualization-ui-components/components/field_picker/index.ts +++ b/packages/kbn-visualization-ui-components/components/field_picker/index.ts @@ -7,5 +7,4 @@ */ export { FieldPicker } from './field_picker'; -export { TruncatedLabel } from './truncated_label'; export type { FieldOptionValue, FieldOption, DataType } from './types'; diff --git a/packages/kbn-visualization-ui-components/components/field_picker/truncated_label.test.tsx b/packages/kbn-visualization-ui-components/components/field_picker/truncated_label.test.tsx deleted file mode 100644 index fb9b7581bbdc..000000000000 --- a/packages/kbn-visualization-ui-components/components/field_picker/truncated_label.test.tsx +++ /dev/null @@ -1,78 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { mount } from 'enzyme'; -import { TruncatedLabel } from './truncated_label'; - -describe('truncated_label', () => { - const defaultProps = { - font: '14px Inter', - // jest-canvas-mock mocks measureText as the number of string characters, thats why the width is so low - width: 30, - search: '', - label: 'example_field', - }; - it('displays passed label if shorter than passed labelLength', () => { - const wrapper = mount(); - expect(wrapper.text()).toEqual('example_field'); - }); - it('middle truncates label', () => { - const wrapper = mount( - - ); - expect(wrapper.text()).toEqual('example_….subcategory.subfield'); - }); - describe('with search value passed', () => { - it('constructs truncated label when searching for the string of index = 0', () => { - const wrapper = mount( - - ); - expect(wrapper.text()).toEqual('example_space.example_field.s…'); - expect(wrapper.find('mark').text()).toEqual('example_space'); - }); - it('constructs truncated label when searching for the string in the middle', () => { - const wrapper = mount( - - ); - expect(wrapper.text()).toEqual('…ample_field.subcategory.subf…'); - expect(wrapper.find('mark').text()).toEqual('ample_field'); - }); - it('constructs truncated label when searching for the string at the end of the label', () => { - const wrapper = mount( - - ); - expect(wrapper.text()).toEqual('…le_field.subcategory.subfield'); - expect(wrapper.find('mark').text()).toEqual('subf'); - }); - - it('constructs truncated label when searching for the string longer than the truncated width and highlights the whole content', () => { - const wrapper = mount( - - ); - expect(wrapper.text()).toEqual('…ample_space.example_field.su…'); - expect(wrapper.find('mark').text()).toEqual('…ample_space.example_field.su…'); - }); - }); -}); diff --git a/packages/kbn-visualization-ui-components/components/field_picker/truncated_label.tsx b/packages/kbn-visualization-ui-components/components/field_picker/truncated_label.tsx deleted file mode 100644 index 4715e25acaf0..000000000000 --- a/packages/kbn-visualization-ui-components/components/field_picker/truncated_label.tsx +++ /dev/null @@ -1,86 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useMemo } from 'react'; -import { EuiMark, EuiHighlight } from '@elastic/eui'; - -const createContext = () => - document.createElement('canvas').getContext('2d') as CanvasRenderingContext2D; - -// extracted from getTextWidth for performance -const context = createContext(); - -const getTextWidth = (text: string, font: string) => { - const ctx = context ?? createContext(); - ctx.font = font; - const metrics = ctx.measureText(text); - return metrics.width; -}; - -const truncateLabel = ( - width: number, - font: string, - label: string, - approximateLength: number, - labelFn: (label: string, length: number) => string -) => { - let output = labelFn(label, approximateLength); - while (getTextWidth(output, font) > width) { - approximateLength = approximateLength - 1; - output = labelFn(label, approximateLength); - } - return output; -}; - -export const TruncatedLabel = React.memo(function TruncatedLabel({ - label, - width, - search, - font, -}: { - label: string; - search: string; - width: number; - font: string; -}) { - const textWidth = useMemo(() => getTextWidth(label, font), [label, font]); - - if (textWidth < width) { - return {label}; - } - - const searchPosition = label.indexOf(search); - const approximateLen = Math.round((width * label.length) / textWidth); - const separator = `…`; - let separatorsLength = separator.length; - let labelFn; - - if (!search || searchPosition === -1) { - labelFn = (text: string, length: number) => - `${text.substr(0, 8)}${separator}${text.substr(text.length - (length - 8))}`; - } else if (searchPosition === 0) { - // search phrase at the beginning - labelFn = (text: string, length: number) => `${text.substr(0, length)}${separator}`; - } else if (approximateLen > label.length - searchPosition) { - // search phrase close to the end or at the end - labelFn = (text: string, length: number) => `${separator}${text.substr(text.length - length)}`; - } else { - // search phrase is in the middle - labelFn = (text: string, length: number) => - `${separator}${text.substr(searchPosition, length)}${separator}`; - separatorsLength = 2 * separator.length; - } - - const outputLabel = truncateLabel(width, font, label, approximateLen, labelFn); - - return search.length < outputLabel.length - separatorsLength ? ( - {outputLabel} - ) : ( - {outputLabel} - ); -}); diff --git a/packages/kbn-visualization-ui-components/index.ts b/packages/kbn-visualization-ui-components/index.ts index 8b9f29fec31a..16c51f96a869 100644 --- a/packages/kbn-visualization-ui-components/index.ts +++ b/packages/kbn-visualization-ui-components/index.ts @@ -8,7 +8,6 @@ export { FieldPicker, - TruncatedLabel, NameInput, DebouncedInput, useDebouncedValue, diff --git a/src/plugins/unified_search/public/filter_badge/filter_badge.tsx b/src/plugins/unified_search/public/filter_badge/filter_badge.tsx index c1cc5c7a3da5..7b20eab971e9 100644 --- a/src/plugins/unified_search/public/filter_badge/filter_badge.tsx +++ b/src/plugins/unified_search/public/filter_badge/filter_badge.tsx @@ -63,6 +63,7 @@ function FilterBadge({ diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx index fb3acc3daa54..f6d539aadfe1 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx @@ -56,7 +56,11 @@ import { } from './lib/filter_editor_utils'; import { FiltersBuilder } from '../../filters_builder'; import { FilterBadgeGroup } from '../../filter_badge/filter_badge_group'; -import { flattenFilters } from './lib/helpers'; +import { + MIDDLE_TRUNCATION_PROPS, + SINGLE_SELECTION_AS_TEXT_PROPS, + flattenFilters, +} from './lib/helpers'; import { filterBadgeStyle, filterPreviewLabelStyle, @@ -301,9 +305,10 @@ class FilterEditorComponent extends Component { selectedOptions={selectedDataView ? [selectedDataView] : []} getLabel={(indexPattern) => indexPattern.getName()} onChange={this.onIndexPatternChange} - singleSelection={{ asPlainText: true }} isClearable={false} data-test-subj="filterIndexPatternsSelect" + singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} + truncationProps={MIDDLE_TRUNCATION_PROPS} /> @@ -371,7 +376,7 @@ class FilterEditorComponent extends Component { } > - + { OPTION_CONTENT_CLASSNAME: string ) => React.ReactNode; inputRef?: ((instance: HTMLInputElement | null) => void) | undefined; + truncationProps?: EuiComboBoxProps['truncationProps']; [propName: string]: any; } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/index.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/index.ts index f1ff6c50b578..4192f415770b 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/index.ts +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/index.ts @@ -36,7 +36,6 @@ export { PhrasesValuesInput } from './phrases_values_input'; export { PhraseValueInput } from './phrase_value_input'; export { RangeValueInput, isRangeParams } from './range_value_input'; export { ValueInputType } from './value_input_type'; -export { TruncatedLabel } from './truncated_label'; export { FilterEditor } from './filter_editor'; export type { FilterEditorProps } from './filter_editor'; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.ts index 53c36525e1d3..31c5c5a9656e 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.ts +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.ts @@ -63,3 +63,6 @@ export const flattenFilters = (filter: Filter[]) => { return returnArray; }; + +export const MIDDLE_TRUNCATION_PROPS = { truncation: 'middle' as const }; +export const SINGLE_SELECTION_AS_TEXT_PROPS = { asPlainText: true }; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx index 0a466c61770c..62648ae50d26 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx @@ -10,11 +10,10 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import { uniq } from 'lodash'; import React from 'react'; import { withKibana } from '@kbn/kibana-react-plugin/public'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { ValueInputType } from './value_input_type'; -import { TruncatedLabel } from './truncated_label'; +import { MIDDLE_TRUNCATION_PROPS, SINGLE_SELECTION_AS_TEXT_PROPS } from './lib/helpers'; interface PhraseValueInputProps extends PhraseSuggestorProps { value?: string; @@ -26,10 +25,6 @@ interface PhraseValueInputProps extends PhraseSuggestorProps { invalid?: boolean; } -const DEFAULT_COMBOBOX_WIDTH = 250; -const COMBOBOX_PADDINGS = 10; -const DEFAULT_FONT = '14px Inter'; - class PhraseValueInputUI extends PhraseSuggestorUI { comboBoxWrapperRef = React.createRef(); inputRef: HTMLInputElement | null = null; @@ -92,24 +87,11 @@ class PhraseValueInputUI extends PhraseSuggestorUI { }); }} onSearchChange={this.onSearchChange} - singleSelection={{ asPlainText: true }} onCreateOption={onChange} isClearable={false} data-test-subj="filterParamsComboBox phraseParamsComboxBox" - renderOption={(option, searchValue) => ( - - - - - - )} + singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} + truncationProps={MIDDLE_TRUNCATION_PROPS} /> ); diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx index b31e6aad7d43..513dad144582 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx @@ -10,11 +10,11 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import { uniq } from 'lodash'; import React from 'react'; import { withKibana } from '@kbn/kibana-react-plugin/public'; -import { EuiFlexGroup, EuiFlexItem, withEuiTheme, WithEuiThemeProps } from '@elastic/eui'; +import { withEuiTheme, WithEuiThemeProps } from '@elastic/eui'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; -import { TruncatedLabel } from './truncated_label'; import { phrasesValuesComboboxCss } from './phrases_values_input.styles'; +import { MIDDLE_TRUNCATION_PROPS } from './lib/helpers'; interface Props { values?: string[]; @@ -27,11 +27,6 @@ interface Props { } export type PhrasesValuesInputProps = Props & PhraseSuggestorProps & WithEuiThemeProps; - -const DEFAULT_COMBOBOX_WIDTH = 250; -const COMBOBOX_PADDINGS = 20; -const DEFAULT_FONT = '14px Inter'; - class PhrasesValuesInputUI extends PhraseSuggestorUI { comboBoxWrapperRef = React.createRef(); @@ -67,20 +62,7 @@ class PhrasesValuesInputUI extends PhraseSuggestorUI { isClearable={false} data-test-subj="filterParamsComboBox phrasesParamsComboxBox" isDisabled={disabled} - renderOption={(option, searchValue) => ( - - - - - - )} + truncationProps={MIDDLE_TRUNCATION_PROPS} /> ); diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.test.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.test.tsx deleted file mode 100644 index 08236041ab93..000000000000 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.test.tsx +++ /dev/null @@ -1,79 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { ComponentProps } from 'react'; -import { mount } from 'enzyme'; -import { TruncatedLabel } from './truncated_label'; - -describe('truncated_label', () => { - const defaultProps: ComponentProps = { - defaultFont: '14px Inter', - defaultComboboxWidth: 130, - comboboxPaddings: 100, - comboBoxWrapperRef: React.createRef(), - search: '', - label: 'example_field', - }; - it('displays passed label if shorter than passed labelLength', () => { - const wrapper = mount(); - expect(wrapper.text()).toEqual('example_field'); - }); - it('middle truncates label', () => { - const wrapper = mount( - - ); - expect(wrapper.text()).toEqual('example_….subcategory.subfield'); - }); - describe('with search value passed', () => { - it('constructs truncated label when searching for the string of index = 0', () => { - const wrapper = mount( - - ); - expect(wrapper.text()).toEqual('example_space.example_field.s…'); - expect(wrapper.find('mark').text()).toEqual('example_space'); - }); - it('constructs truncated label when searching for the string in the middle', () => { - const wrapper = mount( - - ); - expect(wrapper.text()).toEqual('…ample_field.subcategory.subf…'); - expect(wrapper.find('mark').text()).toEqual('ample_field'); - }); - it('constructs truncated label when searching for the string at the end of the label', () => { - const wrapper = mount( - - ); - expect(wrapper.text()).toEqual('…le_field.subcategory.subfield'); - expect(wrapper.find('mark').text()).toEqual('subf'); - }); - - it('constructs truncated label when searching for the string longer than the truncated width and highlights the whole content', () => { - const wrapper = mount( - - ); - expect(wrapper.text()).toEqual('…ample_space.example_field.su…'); - expect(wrapper.find('mark').text()).toEqual('…ample_space.example_field.su…'); - }); - }); -}); diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.tsx deleted file mode 100644 index 21304ad244ed..000000000000 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.tsx +++ /dev/null @@ -1,141 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { RefObject, useMemo } from 'react'; -import useEffectOnce from 'react-use/lib/useEffectOnce'; -import { EuiMark } from '@elastic/eui'; -import { EuiHighlight } from '@elastic/eui'; -import { throttle } from 'lodash'; - -interface TruncatedLabelProps { - label: string; - search: string; - comboBoxWrapperRef: RefObject; - defaultFont: string; - defaultComboboxWidth: number; - comboboxPaddings: number; -} - -const createContext = () => - document.createElement('canvas').getContext('2d') as CanvasRenderingContext2D; - -// extracted from getTextWidth for performance -const context = createContext(); - -const getTextWidth = (text: string, font: string) => { - const ctx = context ?? createContext(); - ctx.font = font; - const metrics = ctx.measureText(text); - return metrics.width; -}; - -const truncateLabel = ( - width: number, - font: string, - label: string, - approximateLength: number, - labelFn: (label: string, length: number) => string -) => { - let output = labelFn(label, approximateLength); - - while (getTextWidth(output, font) > width) { - approximateLength = approximateLength - 1; - const newOutput = labelFn(label, approximateLength); - if (newOutput === output) { - break; - } - output = newOutput; - } - return output; -}; - -export const TruncatedLabel = React.memo(function TruncatedLabel({ - label, - comboBoxWrapperRef, - search, - defaultFont, - defaultComboboxWidth, - comboboxPaddings, -}: TruncatedLabelProps) { - const [labelProps, setLabelProps] = React.useState<{ - width: number; - font: string; - }>({ - width: defaultComboboxWidth - comboboxPaddings, - font: defaultFont, - }); - const computeStyles = (_e: UIEvent | undefined, shouldRecomputeAll = false) => { - if (comboBoxWrapperRef.current) { - const current = { - ...labelProps, - width: comboBoxWrapperRef.current.clientWidth - comboboxPaddings, - }; - if (shouldRecomputeAll) { - current.font = window.getComputedStyle(comboBoxWrapperRef.current).font; - } - setLabelProps(current); - } - }; - - const handleResize = throttle((_e: UIEvent | undefined, shouldRecomputeAll = false) => { - computeStyles(_e, shouldRecomputeAll); - }, 50); - - useEffectOnce(() => { - if (comboBoxWrapperRef.current) { - handleResize(undefined, true); - } - - window.addEventListener('resize', handleResize); - return () => { - window.removeEventListener('resize', handleResize); - }; - }); - - const textWidth = useMemo(() => getTextWidth(label, labelProps.font), [label, labelProps.font]); - - if (textWidth < labelProps.width) { - return {label}; - } - - const searchPosition = label.indexOf(search); - const approximateLen = Math.round((labelProps.width * label.length) / textWidth); - const separator = `…`; - let separatorsLength = separator.length; - let labelFn; - - if (!search || searchPosition === -1) { - labelFn = (text: string, length: number) => - `${text.substr(0, 8)}${separator}${text.substr(text.length - (length - 8))}`; - } else if (searchPosition === 0) { - // search phrase at the beginning - labelFn = (text: string, length: number) => `${text.substr(0, length)}${separator}`; - } else if (approximateLen > label.length - searchPosition) { - // search phrase close to the end or at the end - labelFn = (text: string, length: number) => `${separator}${text.substr(text.length - length)}`; - } else { - // search phrase is in the middle - labelFn = (text: string, length: number) => - `${separator}${text.substr(searchPosition, length)}${separator}`; - separatorsLength = 2 * separator.length; - } - - const outputLabel = truncateLabel( - labelProps.width, - labelProps.font, - label, - approximateLen, - labelFn - ); - - return search.length < outputLabel.length - separatorsLength ? ( - {outputLabel} - ) : ( - {outputLabel} - ); -}); diff --git a/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx index 9c25c7eaf06e..d14891c5fc3f 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx @@ -80,7 +80,7 @@ export type FilterLabelStatus = | typeof FILTER_ITEM_WARNING | typeof FILTER_ITEM_ERROR; -export const FILTER_EDITOR_WIDTH = 960; +export const FILTER_EDITOR_WIDTH = 1200; function FilterItemComponent(props: FilterItemProps) { const { onCloseFilterPopover, onLocalFilterCreate, onLocalFilterUpdate } = props; diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx index 8c3dc65758c2..540226caef52 100644 --- a/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx +++ b/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx @@ -11,20 +11,13 @@ import { i18n } from '@kbn/i18n'; import { FieldIcon } from '@kbn/react-field'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; -import { - EuiFlexGroup, - EuiFlexItem, - useGeneratedHtmlId, - EuiComboBox, - EuiComboBoxOptionOption, -} from '@elastic/eui'; +import { useGeneratedHtmlId, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { getFilterableFields } from '../../filter_bar/filter_editor'; import { FiltersBuilderContextType } from '../context'; -import { TruncatedLabel } from '../../filter_bar/filter_editor'; - -const DEFAULT_COMBOBOX_WIDTH = 205; -const COMBOBOX_PADDINGS = 100; -const DEFAULT_FONT = '14px Inter'; +import { + MIDDLE_TRUNCATION_PROPS, + SINGLE_SELECTION_AS_TEXT_PROPS, +} from '../../filter_bar/filter_editor/lib/helpers'; export const strings = { getFieldSelectPlaceholderLabel: () => @@ -62,6 +55,7 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps) return { label, value: dataViewField.type as KBN_FIELD_TYPES, + prepend: , }; }, [suggestionsAbstraction] @@ -103,30 +97,14 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps) isDisabled={disabled} placeholder={strings.getFieldSelectPlaceholderLabel()} sortMatchesBy="startsWith" - singleSelection={{ asPlainText: true }} aria-label={strings.getFieldSelectPlaceholderLabel()} isClearable={false} compressed fullWidth onFocus={handleFocus} data-test-subj="filterFieldSuggestionList" - renderOption={(option, searchValue) => ( - - - - - - - - - )} + singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} + truncationProps={MIDDLE_TRUNCATION_PROPS} /> ); diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/trigger.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/trigger.tsx index 0669b7b08f07..f0a4648a3606 100644 --- a/x-pack/plugins/lens/public/shared_components/dataview_picker/trigger.tsx +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/trigger.tsx @@ -101,7 +101,7 @@ export function TriggerButton({ fullWidth {...colorProp} {...rest} - textProps={{ style: { width: '100%', lineHeight: '100%' } }} + textProps={{ style: { width: '100%', lineHeight: '1.2em' } }} >