mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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 <img width="984" alt="Screenshot 2023-11-13 at 11 30 20" src="9acb6462
-3205-4e5c-81bd-c3ae10c8323b"> (Unified Search) Value selector: <img width="972" alt="Screenshot 2023-11-13 at 11 30 30" src="e58b09de
-d582-431f-bbd6-97b7c5bd38de"> (Lens) Field picker within layer config: <img width="346" alt="Screenshot 2023-11-09 at 14 44 58" src="4ecb0ea5
-bb01-49e3-a54f-4c8c5884c418"> Also fixes tiny stylistic issue for dataview picker label cut on the bottom: <img width="368" alt="Screenshot 2023-11-09 at 15 06 38" src="b9ae6956
-c1ef-481e-905d-71ffe5e5545a"> <img width="386" alt="Screenshot 2023-11-09 at 15 07 08" src="5d49ed7a
-e8f2-40c1-ac53-a3580b82740e">
This commit is contained in:
parent
29853c79f3
commit
362ef64751
17 changed files with 98 additions and 546 deletions
|
@ -7,14 +7,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './field_picker.scss';
|
import './field_picker.scss';
|
||||||
import React, { useRef } from 'react';
|
import React from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import useEffectOnce from 'react-use/lib/useEffectOnce';
|
import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui';
|
||||||
import { EuiComboBox, EuiComboBoxProps, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|
||||||
import { FieldIcon } from '@kbn/field-utils/src/components/field_icon';
|
import { FieldIcon } from '@kbn/field-utils/src/components/field_icon';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { DataType } from './types';
|
|
||||||
import { TruncatedLabel } from './truncated_label';
|
|
||||||
import type { FieldOptionValue, FieldOption } from './types';
|
import type { FieldOptionValue, FieldOption } from './types';
|
||||||
|
|
||||||
export interface FieldPickerProps<T extends FieldOptionValue>
|
export interface FieldPickerProps<T extends FieldOptionValue>
|
||||||
|
@ -27,9 +24,8 @@ export interface FieldPickerProps<T extends FieldOptionValue>
|
||||||
'data-test-subj'?: string;
|
'data-test-subj'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_COMBOBOX_WIDTH = 305;
|
const MIDDLE_TRUNCATION_PROPS = { truncation: 'middle' as const };
|
||||||
const COMBOBOX_PADDINGS = 90;
|
const SINGLE_SELECTION_AS_TEXT_PROPS = { asPlainText: true };
|
||||||
const DEFAULT_FONT = '14px Inter';
|
|
||||||
|
|
||||||
export function FieldPicker<T extends FieldOptionValue = FieldOptionValue>({
|
export function FieldPicker<T extends FieldOptionValue = FieldOptionValue>({
|
||||||
selectedOptions,
|
selectedOptions,
|
||||||
|
@ -40,95 +36,86 @@ export function FieldPicker<T extends FieldOptionValue = FieldOptionValue>({
|
||||||
['data-test-subj']: dataTestSub,
|
['data-test-subj']: dataTestSub,
|
||||||
...rest
|
...rest
|
||||||
}: FieldPickerProps<T>) {
|
}: FieldPickerProps<T>) {
|
||||||
|
let theLongestLabel = '';
|
||||||
const styledOptions = options?.map(({ compatible, exists, ...otherAttr }) => {
|
const styledOptions = options?.map(({ compatible, exists, ...otherAttr }) => {
|
||||||
if (otherAttr.options) {
|
if (otherAttr.options) {
|
||||||
return {
|
return {
|
||||||
...otherAttr,
|
...otherAttr,
|
||||||
options: otherAttr.options.map(({ exists: fieldOptionExists, ...fieldOption }) => ({
|
options: otherAttr.options.map(({ exists: fieldOptionExists, ...fieldOption }) => {
|
||||||
...fieldOption,
|
if (fieldOption.label.length > theLongestLabel.length) {
|
||||||
className: classNames({
|
theLongestLabel = fieldOption.label;
|
||||||
'lnFieldPicker__option--incompatible': !fieldOption.compatible,
|
}
|
||||||
'lnFieldPicker__option--nonExistant': !fieldOptionExists,
|
return {
|
||||||
}),
|
...fieldOption,
|
||||||
})),
|
prepend: fieldOption.value.dataType ? (
|
||||||
|
<FieldIcon
|
||||||
|
type={fieldOption.value.dataType}
|
||||||
|
fill="none"
|
||||||
|
className="eui-alignMiddle"
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
className: classNames({
|
||||||
|
'lnFieldPicker__option--incompatible': !fieldOption.compatible,
|
||||||
|
'lnFieldPicker__option--nonExistant': !fieldOptionExists,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...otherAttr,
|
...otherAttr,
|
||||||
compatible,
|
compatible,
|
||||||
|
prepend: otherAttr.value.dataType ? (
|
||||||
|
<FieldIcon type={otherAttr.value.dataType} fill="none" className="eui-alignMiddle" />
|
||||||
|
) : null,
|
||||||
className: classNames({
|
className: classNames({
|
||||||
'lnFieldPicker__option--incompatible': !compatible,
|
'lnFieldPicker__option--incompatible': !compatible,
|
||||||
'lnFieldPicker__option--nonExistant': !exists,
|
'lnFieldPicker__option--nonExistant': !exists,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const comboBoxRef = useRef<HTMLInputElement>(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 (
|
return (
|
||||||
<div ref={comboBoxRef}>
|
<EuiComboBox
|
||||||
<EuiComboBox
|
fullWidth
|
||||||
fullWidth
|
compressed
|
||||||
compressed
|
isClearable={false}
|
||||||
isClearable={false}
|
data-test-subj={dataTestSub ?? 'indexPattern-dimension-field'}
|
||||||
data-test-subj={dataTestSub ?? 'indexPattern-dimension-field'}
|
placeholder={i18n.translate('visualizationUiComponents.fieldPicker.fieldPlaceholder', {
|
||||||
placeholder={i18n.translate('visualizationUiComponents.fieldPicker.fieldPlaceholder', {
|
defaultMessage: 'Select a field',
|
||||||
defaultMessage: 'Select a field',
|
})}
|
||||||
})}
|
options={styledOptions}
|
||||||
options={styledOptions}
|
isInvalid={fieldIsInvalid}
|
||||||
isInvalid={fieldIsInvalid}
|
selectedOptions={selectedOptions}
|
||||||
selectedOptions={selectedOptions}
|
singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS}
|
||||||
singleSelection={{ asPlainText: true }}
|
truncationProps={MIDDLE_TRUNCATION_PROPS}
|
||||||
onChange={(choices) => {
|
inputPopoverProps={{ panelMinWidth }}
|
||||||
if (choices.length === 0) {
|
onChange={(choices) => {
|
||||||
onDelete?.();
|
if (choices.length === 0) {
|
||||||
return;
|
onDelete?.();
|
||||||
}
|
return;
|
||||||
onChoose(choices[0].value);
|
}
|
||||||
}}
|
onChoose(choices[0].value);
|
||||||
renderOption={(option, searchValue) => {
|
}}
|
||||||
return (
|
{...rest}
|
||||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
/>
|
||||||
<EuiFlexItem grow={null}>
|
|
||||||
<FieldIcon
|
|
||||||
type={(option.value as unknown as { dataType: DataType }).dataType}
|
|
||||||
fill="none"
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
<EuiFlexItem>
|
|
||||||
<TruncatedLabel {...labelProps} label={option.label} search={searchValue} />
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -7,5 +7,4 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { FieldPicker } from './field_picker';
|
export { FieldPicker } from './field_picker';
|
||||||
export { TruncatedLabel } from './truncated_label';
|
|
||||||
export type { FieldOptionValue, FieldOption, DataType } from './types';
|
export type { FieldOptionValue, FieldOption, DataType } from './types';
|
||||||
|
|
|
@ -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(<TruncatedLabel {...defaultProps} />);
|
|
||||||
expect(wrapper.text()).toEqual('example_field');
|
|
||||||
});
|
|
||||||
it('middle truncates label', () => {
|
|
||||||
const wrapper = mount(
|
|
||||||
<TruncatedLabel {...defaultProps} label="example_space.example_field.subcategory.subfield" />
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<TruncatedLabel
|
|
||||||
{...defaultProps}
|
|
||||||
search="example_space"
|
|
||||||
label="example_space.example_field.subcategory.subfield"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<TruncatedLabel
|
|
||||||
{...defaultProps}
|
|
||||||
search={'ample_field'}
|
|
||||||
label="example_space.example_field.subcategory.subfield"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<TruncatedLabel
|
|
||||||
{...defaultProps}
|
|
||||||
search={'subf'}
|
|
||||||
label="example_space.example_field.subcategory.subfield"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<TruncatedLabel
|
|
||||||
{...defaultProps}
|
|
||||||
search={'ample_space.example_field.subcategory.subfie'}
|
|
||||||
label="example_space.example_field.subcategory.subfield"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(wrapper.text()).toEqual('…ample_space.example_field.su…');
|
|
||||||
expect(wrapper.find('mark').text()).toEqual('…ample_space.example_field.su…');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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 <EuiHighlight search={search}>{label}</EuiHighlight>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ? (
|
|
||||||
<EuiHighlight search={search}>{outputLabel}</EuiHighlight>
|
|
||||||
) : (
|
|
||||||
<EuiMark>{outputLabel}</EuiMark>
|
|
||||||
);
|
|
||||||
});
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
export {
|
export {
|
||||||
FieldPicker,
|
FieldPicker,
|
||||||
TruncatedLabel,
|
|
||||||
NameInput,
|
NameInput,
|
||||||
DebouncedInput,
|
DebouncedInput,
|
||||||
useDebouncedValue,
|
useDebouncedValue,
|
||||||
|
|
|
@ -63,6 +63,7 @@ function FilterBadge({
|
||||||
<span
|
<span
|
||||||
css={css`
|
css={css`
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
overflow-wrap: break-word;
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<EuiTextBlockTruncate lines={10}>
|
<EuiTextBlockTruncate lines={10}>
|
||||||
|
|
|
@ -56,7 +56,11 @@ import {
|
||||||
} from './lib/filter_editor_utils';
|
} from './lib/filter_editor_utils';
|
||||||
import { FiltersBuilder } from '../../filters_builder';
|
import { FiltersBuilder } from '../../filters_builder';
|
||||||
import { FilterBadgeGroup } from '../../filter_badge/filter_badge_group';
|
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 {
|
import {
|
||||||
filterBadgeStyle,
|
filterBadgeStyle,
|
||||||
filterPreviewLabelStyle,
|
filterPreviewLabelStyle,
|
||||||
|
@ -301,9 +305,10 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
|
||||||
selectedOptions={selectedDataView ? [selectedDataView] : []}
|
selectedOptions={selectedDataView ? [selectedDataView] : []}
|
||||||
getLabel={(indexPattern) => indexPattern.getName()}
|
getLabel={(indexPattern) => indexPattern.getName()}
|
||||||
onChange={this.onIndexPatternChange}
|
onChange={this.onIndexPatternChange}
|
||||||
singleSelection={{ asPlainText: true }}
|
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
data-test-subj="filterIndexPatternsSelect"
|
data-test-subj="filterIndexPatternsSelect"
|
||||||
|
singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS}
|
||||||
|
truncationProps={MIDDLE_TRUNCATION_PROPS}
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
<EuiSpacer size="s" />
|
<EuiSpacer size="s" />
|
||||||
|
@ -371,7 +376,7 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
|
||||||
</strong>
|
</strong>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<EuiText size="xs" data-test-subj="filter-preview">
|
<EuiText size="xs" data-test-subj="filter-preview" css={{ overflowWrap: 'break-word' }}>
|
||||||
<FilterBadgeGroup
|
<FilterBadgeGroup
|
||||||
filters={[localFilter]}
|
filters={[localFilter]}
|
||||||
dataViews={this.props.indexPatterns}
|
dataViews={this.props.indexPatterns}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiComboBox, EuiComboBoxOptionOption, useEuiTheme } from '@elastic/eui';
|
import { EuiComboBox, EuiComboBoxOptionOption, useEuiTheme, EuiComboBoxProps } from '@elastic/eui';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { genericComboBoxStyle } from './generic_combo_box.styles';
|
import { genericComboBoxStyle } from './generic_combo_box.styles';
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ export interface GenericComboBoxProps<T> {
|
||||||
OPTION_CONTENT_CLASSNAME: string
|
OPTION_CONTENT_CLASSNAME: string
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
inputRef?: ((instance: HTMLInputElement | null) => void) | undefined;
|
inputRef?: ((instance: HTMLInputElement | null) => void) | undefined;
|
||||||
|
truncationProps?: EuiComboBoxProps<T>['truncationProps'];
|
||||||
[propName: string]: any;
|
[propName: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,6 @@ export { PhrasesValuesInput } from './phrases_values_input';
|
||||||
export { PhraseValueInput } from './phrase_value_input';
|
export { PhraseValueInput } from './phrase_value_input';
|
||||||
export { RangeValueInput, isRangeParams } from './range_value_input';
|
export { RangeValueInput, isRangeParams } from './range_value_input';
|
||||||
export { ValueInputType } from './value_input_type';
|
export { ValueInputType } from './value_input_type';
|
||||||
export { TruncatedLabel } from './truncated_label';
|
|
||||||
|
|
||||||
export { FilterEditor } from './filter_editor';
|
export { FilterEditor } from './filter_editor';
|
||||||
export type { FilterEditorProps } from './filter_editor';
|
export type { FilterEditorProps } from './filter_editor';
|
||||||
|
|
|
@ -63,3 +63,6 @@ export const flattenFilters = (filter: Filter[]) => {
|
||||||
|
|
||||||
return returnArray;
|
return returnArray;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MIDDLE_TRUNCATION_PROPS = { truncation: 'middle' as const };
|
||||||
|
export const SINGLE_SELECTION_AS_TEXT_PROPS = { asPlainText: true };
|
||||||
|
|
|
@ -10,11 +10,10 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
|
||||||
import { uniq } from 'lodash';
|
import { uniq } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withKibana } from '@kbn/kibana-react-plugin/public';
|
import { withKibana } from '@kbn/kibana-react-plugin/public';
|
||||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|
||||||
import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box';
|
import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box';
|
||||||
import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor';
|
import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor';
|
||||||
import { ValueInputType } from './value_input_type';
|
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 {
|
interface PhraseValueInputProps extends PhraseSuggestorProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
|
@ -26,10 +25,6 @@ interface PhraseValueInputProps extends PhraseSuggestorProps {
|
||||||
invalid?: boolean;
|
invalid?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_COMBOBOX_WIDTH = 250;
|
|
||||||
const COMBOBOX_PADDINGS = 10;
|
|
||||||
const DEFAULT_FONT = '14px Inter';
|
|
||||||
|
|
||||||
class PhraseValueInputUI extends PhraseSuggestorUI<PhraseValueInputProps> {
|
class PhraseValueInputUI extends PhraseSuggestorUI<PhraseValueInputProps> {
|
||||||
comboBoxWrapperRef = React.createRef<HTMLDivElement>();
|
comboBoxWrapperRef = React.createRef<HTMLDivElement>();
|
||||||
inputRef: HTMLInputElement | null = null;
|
inputRef: HTMLInputElement | null = null;
|
||||||
|
@ -92,24 +87,11 @@ class PhraseValueInputUI extends PhraseSuggestorUI<PhraseValueInputProps> {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onSearchChange={this.onSearchChange}
|
onSearchChange={this.onSearchChange}
|
||||||
singleSelection={{ asPlainText: true }}
|
|
||||||
onCreateOption={onChange}
|
onCreateOption={onChange}
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
data-test-subj="filterParamsComboBox phraseParamsComboxBox"
|
data-test-subj="filterParamsComboBox phraseParamsComboxBox"
|
||||||
renderOption={(option, searchValue) => (
|
singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS}
|
||||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
truncationProps={MIDDLE_TRUNCATION_PROPS}
|
||||||
<EuiFlexItem>
|
|
||||||
<TruncatedLabel
|
|
||||||
defaultComboboxWidth={DEFAULT_COMBOBOX_WIDTH}
|
|
||||||
defaultFont={DEFAULT_FONT}
|
|
||||||
comboboxPaddings={COMBOBOX_PADDINGS}
|
|
||||||
comboBoxWrapperRef={this.comboBoxWrapperRef}
|
|
||||||
label={option.label}
|
|
||||||
search={searchValue}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,11 +10,11 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
|
||||||
import { uniq } from 'lodash';
|
import { uniq } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withKibana } from '@kbn/kibana-react-plugin/public';
|
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 { GenericComboBox, GenericComboBoxProps } from './generic_combo_box';
|
||||||
import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor';
|
import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor';
|
||||||
import { TruncatedLabel } from './truncated_label';
|
|
||||||
import { phrasesValuesComboboxCss } from './phrases_values_input.styles';
|
import { phrasesValuesComboboxCss } from './phrases_values_input.styles';
|
||||||
|
import { MIDDLE_TRUNCATION_PROPS } from './lib/helpers';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
values?: string[];
|
values?: string[];
|
||||||
|
@ -27,11 +27,6 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PhrasesValuesInputProps = Props & PhraseSuggestorProps & WithEuiThemeProps;
|
export type PhrasesValuesInputProps = Props & PhraseSuggestorProps & WithEuiThemeProps;
|
||||||
|
|
||||||
const DEFAULT_COMBOBOX_WIDTH = 250;
|
|
||||||
const COMBOBOX_PADDINGS = 20;
|
|
||||||
const DEFAULT_FONT = '14px Inter';
|
|
||||||
|
|
||||||
class PhrasesValuesInputUI extends PhraseSuggestorUI<PhrasesValuesInputProps> {
|
class PhrasesValuesInputUI extends PhraseSuggestorUI<PhrasesValuesInputProps> {
|
||||||
comboBoxWrapperRef = React.createRef<HTMLDivElement>();
|
comboBoxWrapperRef = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
@ -67,20 +62,7 @@ class PhrasesValuesInputUI extends PhraseSuggestorUI<PhrasesValuesInputProps> {
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
data-test-subj="filterParamsComboBox phrasesParamsComboxBox"
|
data-test-subj="filterParamsComboBox phrasesParamsComboxBox"
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
renderOption={(option, searchValue) => (
|
truncationProps={MIDDLE_TRUNCATION_PROPS}
|
||||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
|
||||||
<EuiFlexItem>
|
|
||||||
<TruncatedLabel
|
|
||||||
defaultComboboxWidth={DEFAULT_COMBOBOX_WIDTH}
|
|
||||||
defaultFont={DEFAULT_FONT}
|
|
||||||
comboboxPaddings={COMBOBOX_PADDINGS}
|
|
||||||
comboBoxWrapperRef={this.comboBoxWrapperRef}
|
|
||||||
label={option.label}
|
|
||||||
search={searchValue}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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<typeof TruncatedLabel> = {
|
|
||||||
defaultFont: '14px Inter',
|
|
||||||
defaultComboboxWidth: 130,
|
|
||||||
comboboxPaddings: 100,
|
|
||||||
comboBoxWrapperRef: React.createRef<HTMLDivElement>(),
|
|
||||||
search: '',
|
|
||||||
label: 'example_field',
|
|
||||||
};
|
|
||||||
it('displays passed label if shorter than passed labelLength', () => {
|
|
||||||
const wrapper = mount(<TruncatedLabel {...defaultProps} />);
|
|
||||||
expect(wrapper.text()).toEqual('example_field');
|
|
||||||
});
|
|
||||||
it('middle truncates label', () => {
|
|
||||||
const wrapper = mount(
|
|
||||||
<TruncatedLabel {...defaultProps} label="example_space.example_field.subcategory.subfield" />
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<TruncatedLabel
|
|
||||||
{...defaultProps}
|
|
||||||
search="example_space"
|
|
||||||
label="example_space.example_field.subcategory.subfield"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<TruncatedLabel
|
|
||||||
{...defaultProps}
|
|
||||||
search={'ample_field'}
|
|
||||||
label="example_space.example_field.subcategory.subfield"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<TruncatedLabel
|
|
||||||
{...defaultProps}
|
|
||||||
search={'subf'}
|
|
||||||
label="example_space.example_field.subcategory.subfield"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<TruncatedLabel
|
|
||||||
{...defaultProps}
|
|
||||||
search={'ample_space.example_field.subcategory.subfie'}
|
|
||||||
label="example_space.example_field.subcategory.subfield"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(wrapper.text()).toEqual('…ample_space.example_field.su…');
|
|
||||||
expect(wrapper.find('mark').text()).toEqual('…ample_space.example_field.su…');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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<HTMLDivElement | null>;
|
|
||||||
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 <EuiHighlight search={search}>{label}</EuiHighlight>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ? (
|
|
||||||
<EuiHighlight search={search}>{outputLabel}</EuiHighlight>
|
|
||||||
) : (
|
|
||||||
<EuiMark>{outputLabel}</EuiMark>
|
|
||||||
);
|
|
||||||
});
|
|
|
@ -80,7 +80,7 @@ export type FilterLabelStatus =
|
||||||
| typeof FILTER_ITEM_WARNING
|
| typeof FILTER_ITEM_WARNING
|
||||||
| typeof FILTER_ITEM_ERROR;
|
| typeof FILTER_ITEM_ERROR;
|
||||||
|
|
||||||
export const FILTER_EDITOR_WIDTH = 960;
|
export const FILTER_EDITOR_WIDTH = 1200;
|
||||||
|
|
||||||
function FilterItemComponent(props: FilterItemProps) {
|
function FilterItemComponent(props: FilterItemProps) {
|
||||||
const { onCloseFilterPopover, onLocalFilterCreate, onLocalFilterUpdate } = props;
|
const { onCloseFilterPopover, onLocalFilterCreate, onLocalFilterUpdate } = props;
|
||||||
|
|
|
@ -11,20 +11,13 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { FieldIcon } from '@kbn/react-field';
|
import { FieldIcon } from '@kbn/react-field';
|
||||||
import { KBN_FIELD_TYPES } from '@kbn/field-types';
|
import { KBN_FIELD_TYPES } from '@kbn/field-types';
|
||||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
|
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
|
||||||
import {
|
import { useGeneratedHtmlId, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||||
EuiFlexGroup,
|
|
||||||
EuiFlexItem,
|
|
||||||
useGeneratedHtmlId,
|
|
||||||
EuiComboBox,
|
|
||||||
EuiComboBoxOptionOption,
|
|
||||||
} from '@elastic/eui';
|
|
||||||
import { getFilterableFields } from '../../filter_bar/filter_editor';
|
import { getFilterableFields } from '../../filter_bar/filter_editor';
|
||||||
import { FiltersBuilderContextType } from '../context';
|
import { FiltersBuilderContextType } from '../context';
|
||||||
import { TruncatedLabel } from '../../filter_bar/filter_editor';
|
import {
|
||||||
|
MIDDLE_TRUNCATION_PROPS,
|
||||||
const DEFAULT_COMBOBOX_WIDTH = 205;
|
SINGLE_SELECTION_AS_TEXT_PROPS,
|
||||||
const COMBOBOX_PADDINGS = 100;
|
} from '../../filter_bar/filter_editor/lib/helpers';
|
||||||
const DEFAULT_FONT = '14px Inter';
|
|
||||||
|
|
||||||
export const strings = {
|
export const strings = {
|
||||||
getFieldSelectPlaceholderLabel: () =>
|
getFieldSelectPlaceholderLabel: () =>
|
||||||
|
@ -62,6 +55,7 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps)
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: dataViewField.type as KBN_FIELD_TYPES,
|
value: dataViewField.type as KBN_FIELD_TYPES,
|
||||||
|
prepend: <FieldIcon type={dataViewField.type} fill="none" className="eui-alignMiddle" />,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[suggestionsAbstraction]
|
[suggestionsAbstraction]
|
||||||
|
@ -103,30 +97,14 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps)
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
placeholder={strings.getFieldSelectPlaceholderLabel()}
|
placeholder={strings.getFieldSelectPlaceholderLabel()}
|
||||||
sortMatchesBy="startsWith"
|
sortMatchesBy="startsWith"
|
||||||
singleSelection={{ asPlainText: true }}
|
|
||||||
aria-label={strings.getFieldSelectPlaceholderLabel()}
|
aria-label={strings.getFieldSelectPlaceholderLabel()}
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
compressed
|
compressed
|
||||||
fullWidth
|
fullWidth
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
data-test-subj="filterFieldSuggestionList"
|
data-test-subj="filterFieldSuggestionList"
|
||||||
renderOption={(option, searchValue) => (
|
singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS}
|
||||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
truncationProps={MIDDLE_TRUNCATION_PROPS}
|
||||||
<EuiFlexItem grow={null}>
|
|
||||||
<FieldIcon type={option.value!} fill="none" />
|
|
||||||
</EuiFlexItem>
|
|
||||||
<EuiFlexItem>
|
|
||||||
<TruncatedLabel
|
|
||||||
defaultComboboxWidth={DEFAULT_COMBOBOX_WIDTH}
|
|
||||||
defaultFont={DEFAULT_FONT}
|
|
||||||
comboboxPaddings={COMBOBOX_PADDINGS}
|
|
||||||
comboBoxWrapperRef={comboBoxWrapperRef}
|
|
||||||
label={option.label}
|
|
||||||
search={searchValue}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -101,7 +101,7 @@ export function TriggerButton({
|
||||||
fullWidth
|
fullWidth
|
||||||
{...colorProp}
|
{...colorProp}
|
||||||
{...rest}
|
{...rest}
|
||||||
textProps={{ style: { width: '100%', lineHeight: '100%' } }}
|
textProps={{ style: { width: '100%', lineHeight: '1.2em' } }}
|
||||||
>
|
>
|
||||||
<TriggerLabel label={label} extraIcons={extraIcons} />
|
<TriggerLabel label={label} extraIcons={extraIcons} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue