[Lens][Unified search] Auto expand comboboxes and popovers based on the content (#171573)

## Summary

Fixes partially two remaining tasks from
https://github.com/elastic/kibana/issues/168753
Fixes partially dataview issue from
https://github.com/elastic/kibana/issues/170398
It stretches to maximum approximate 60 characters if any of the labels
in the list is of this length. If the content doesn't need the container
to stretch, it doesn't do it.


<details>
 <summary> Field picker in Lens</summary>

minimum width:
<img width="445" alt="Screenshot 2023-11-21 at 15 56 03"
src="2f0f8482-bd00-4ec2-bbde-cbc4f3198eed">

auto-expanded width: 
<img width="575" alt="Screenshot 2023-11-21 at 15 58 22"
src="df7bab4d-0a08-4d49-8a91-9386eba15d93">
</details>


<details>
  <summary>Layer data view picker in Lens</summary>
<img width="376" alt="Screenshot 2023-11-21 at 16 01 17"
src="b8a98d83-dabc-49bd-a3cc-fc3856de6d3e">
<img width="455" alt="Screenshot 2023-11-21 at 15 58 09"
src="f2c5bde8-7a4a-485f-bf97-fc2179171e50">
<img width="615" alt="Screenshot 2023-11-21 at 15 56 27"
src="0574fc6c-69a3-44e9-9d48-8d427c1c5dba">
</details>

<details> <summary>Data view picker in Unified Search</summary>
<img width="341" alt="Screenshot 2023-11-21 at 16 00 29"
src="1c838ded-0dc5-4632-94e4-1d94586f667c">
<img width="441" alt="Screenshot 2023-11-21 at 15 58 04"
src="87e4f1c0-7922-4b94-a114-f23ece544395">
<img width="561" alt="Screenshot 2023-11-21 at 15 56 20"
src="3ea0f222-5241-4c5b-b00b-4311972754cc">
</details>

<details>
 <summary> Data view picker in dashboard Create control flyout</summary>
<img width="677" alt="Screenshot 2023-11-21 at 16 14 00"
src="0455b6ed-555d-4cff-9e34-0de377be6e04">
<img width="682" alt="Screenshot 2023-11-21 at 15 54 56"
src="2a67685c-379d-4c0b-bf56-dbf7c35b3bd4">
</details>

<details> 
<summary> Unified search data view select component (tested in
maps)</summary>
<img width="570" alt="Screenshot 2023-11-22 at 14 38 25"
src="bb52ab22-626d-4556-b40c-c9bcc925f426">
</details>

<details>
<summary>Unified search field and value picker</summary>
Adds `panelMinWidth`, removes the custom flex width change behavior
<img width="1142" alt="Screenshot 2023-11-22 at 14 40 26"
src="2450957f-38b7-4a73-b531-7acb29cb56bc">


f4f33624-9287-403e-8472-81f705440f97

</details>

<details> 
<summary> Discover breakdown field</summary>

Removes the focus stretching and instead uses the panelMinWidth prop

<img width="419" alt="Screenshot 2023-11-21 at 16 46 50"
src="e35125ad-8823-4bff-954b-8119a352829c">
<img width="619" alt="Screenshot 2023-11-21 at 16 48 20"
src="89e63daf-a59e-43e1-a6ec-91d1b15b0fcd">


</details>

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marta Bondyra 2023-11-28 13:13:43 +01:00 committed by GitHub
parent a4aa7117bb
commit 085878c289
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 519 additions and 188 deletions

1
.github/CODEOWNERS vendored
View file

@ -55,6 +55,7 @@ packages/kbn-bazel-runner @elastic/kibana-operations
examples/bfetch_explorer @elastic/appex-sharedux
src/plugins/bfetch @elastic/appex-sharedux
packages/kbn-calculate-auto @elastic/obs-ux-management-team
packages/kbn-calculate-width-from-char-count @elastic/kibana-visualizations
x-pack/plugins/canvas @elastic/kibana-presentation
x-pack/test/cases_api_integration/common/plugins/cases @elastic/response-ops
packages/kbn-cases-components @elastic/response-ops

View file

@ -167,6 +167,7 @@
"@kbn/bfetch-explorer-plugin": "link:examples/bfetch_explorer",
"@kbn/bfetch-plugin": "link:src/plugins/bfetch",
"@kbn/calculate-auto": "link:packages/kbn-calculate-auto",
"@kbn/calculate-width-from-char-count": "link:packages/kbn-calculate-width-from-char-count",
"@kbn/canvas-plugin": "link:x-pack/plugins/canvas",
"@kbn/cases-api-integration-test-plugin": "link:x-pack/test/cases_api_integration/common/plugins/cases",
"@kbn/cases-components": "link:packages/kbn-cases-components",

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
module.exports = require('@kbn/storybook').defaultConfig;

View file

@ -0,0 +1,3 @@
# @kbn/calculate-width-from-char-count
This package contains a function that calculates the approximate width of the component from a text length.

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export * from './src';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-calculate-width-from-char-count'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/calculate-width-from-char-count",
"owner": "@elastic/kibana-visualizations"
}

View file

@ -0,0 +1,7 @@
{
"name": "@kbn/calculate-width-from-char-count",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0",
"sideEffects": false
}

View file

@ -0,0 +1,21 @@
/*
* 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 { calculateWidthFromCharCount, MAX_WIDTH } from './calculate_width_from_char_count';
describe('calculateWidthFromCharCount', () => {
it('should return minimum width if char count is smaller than minWidth', () => {
expect(calculateWidthFromCharCount(10, { minWidth: 300 })).toBe(300);
});
it('should return calculated width', () => {
expect(calculateWidthFromCharCount(30)).toBe(30 * 7 + 116);
});
it('should return maximum width if char count is bigger than maxWidth', () => {
expect(calculateWidthFromCharCount(1000)).toBe(MAX_WIDTH);
});
});

View file

@ -0,0 +1,41 @@
/*
* 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.
*/
export interface LIMITS {
paddingsWidth: number;
minWidth?: number;
avCharWidth: number;
maxWidth: number;
}
export const MAX_WIDTH = 550;
const PADDINGS_WIDTH = 116;
const AVERAGE_CHAR_WIDTH = 7;
const defaultPanelWidths: LIMITS = {
maxWidth: MAX_WIDTH,
avCharWidth: AVERAGE_CHAR_WIDTH,
paddingsWidth: PADDINGS_WIDTH,
};
export function calculateWidthFromCharCount(
labelLength: number,
overridesPanelWidths?: Partial<LIMITS>
) {
const { maxWidth, avCharWidth, paddingsWidth, minWidth } = {
...defaultPanelWidths,
...overridesPanelWidths,
};
const widthForCharCount = paddingsWidth + labelLength * avCharWidth;
if (minWidth && widthForCharCount < minWidth) {
return minWidth;
}
return Math.min(widthForCharCount, maxWidth);
}

View file

@ -0,0 +1,53 @@
/*
* 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 { calculateWidthFromEntries } from './calculate_width_from_entries';
import { MAX_WIDTH } from './calculate_width_from_char_count';
import faker from 'faker';
const generateLabel = (length: number) => faker.random.alpha({ count: length });
const generateObjectWithLabelOfLength = (length: number, propOverrides?: Record<string, any>) => ({
label: generateLabel(length),
...propOverrides,
});
describe('calculateWidthFromEntries', () => {
it('calculates width for array of strings', () => {
const shortLabels = [10, 20].map(generateLabel);
expect(calculateWidthFromEntries(shortLabels)).toBe(256);
const mediumLabels = [50, 55, 10, 20].map(generateLabel);
expect(calculateWidthFromEntries(mediumLabels)).toBe(501);
const longLabels = [80, 90, 10].map(generateLabel);
expect(calculateWidthFromEntries(longLabels)).toBe(MAX_WIDTH);
});
it('calculates width for array of objects with keys', () => {
const shortLabels = [10, 20].map((v) => generateObjectWithLabelOfLength(v));
expect(calculateWidthFromEntries(shortLabels, ['label'])).toBe(256);
const mediumLabels = [50, 55, 10, 20].map((v) => generateObjectWithLabelOfLength(v));
expect(calculateWidthFromEntries(mediumLabels, ['label'])).toBe(501);
const longLabels = [80, 90, 10].map((v) => generateObjectWithLabelOfLength(v));
expect(calculateWidthFromEntries(longLabels, ['label'])).toBe(MAX_WIDTH);
});
it('calculates width for array of objects for fallback keys', () => {
const shortLabels = [10, 20].map((v) =>
generateObjectWithLabelOfLength(v, { label: undefined, name: generateLabel(v) })
);
expect(calculateWidthFromEntries(shortLabels, ['id', 'label', 'name'])).toBe(256);
const mediumLabels = [50, 55, 10, 20].map((v) =>
generateObjectWithLabelOfLength(v, { label: undefined, name: generateLabel(v) })
);
expect(calculateWidthFromEntries(mediumLabels, ['id', 'label', 'name'])).toBe(501);
});
});

View file

@ -0,0 +1,39 @@
/*
* 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 { LIMITS, calculateWidthFromCharCount } from './calculate_width_from_char_count';
type GenericObject<T = Record<string, any>> = T;
const getMaxLabelLengthForObjects = (
entries: GenericObject[],
labelKeys: Array<keyof GenericObject>
) =>
entries.reduce((acc, curr) => {
const labelKey = labelKeys.find((key) => curr[key]);
if (!labelKey) {
return acc;
}
const labelLength = curr[labelKey].length;
return acc > labelLength ? acc : labelLength;
}, 0);
const getMaxLabelLengthForStrings = (arr: string[]) =>
arr.reduce((acc, curr) => (acc > curr.length ? acc : curr.length), 0);
export function calculateWidthFromEntries(
entries: GenericObject[] | string[],
labelKeys?: Array<keyof GenericObject>,
overridesPanelWidths?: Partial<LIMITS>
) {
const maxLabelLength = labelKeys
? getMaxLabelLengthForObjects(entries as GenericObject[], labelKeys)
: getMaxLabelLengthForStrings(entries as string[]);
return calculateWidthFromCharCount(maxLabelLength, overridesPanelWidths);
}

View file

@ -0,0 +1,11 @@
/*
* 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.
*/
export { calculateWidthFromCharCount } from './calculate_width_from_char_count';
export { calculateWidthFromEntries } from './calculate_width_from_entries';

View file

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react",
],
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"kbn_references": [],
"exclude": [
"target/**/*",
]
}

View file

@ -0,0 +1,89 @@
/*
* 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 { FieldPicker, FieldPickerProps } from './field_picker';
import { render, screen } from '@testing-library/react';
import faker from 'faker';
import userEvent from '@testing-library/user-event';
import { DataType, FieldOptionValue } from './types';
const generateFieldWithLabelOfLength = (length: number) => ({
label: faker.random.alpha({ count: length }),
value: {
type: 'field' as const,
field: faker.random.alpha({ count: length }),
dataType: 'date' as DataType,
operationType: 'count',
},
exists: true,
compatible: 1,
});
const generateProps = (customField = generateFieldWithLabelOfLength(20)) =>
({
selectedOptions: [
{
label: 'Category',
value: {
type: 'field' as const,
field: 'category.keyword',
dataType: 'keyword' as DataType,
operationType: 'count',
},
},
],
options: [
{
label: 'nested options',
exists: true,
compatible: 1,
value: generateFieldWithLabelOfLength(20),
options: [
generateFieldWithLabelOfLength(20),
customField,
generateFieldWithLabelOfLength(20),
],
},
],
onChoose: jest.fn(),
fieldIsInvalid: false,
} as unknown as FieldPickerProps<FieldOptionValue>);
describe('field picker', () => {
const renderFieldPicker = (customField = generateFieldWithLabelOfLength(20)) => {
const props = generateProps(customField);
const rtlRender = render(<FieldPicker {...props} />);
return {
openCombobox: () => userEvent.click(screen.getByLabelText(/open list of options/i)),
...rtlRender,
};
};
it('should render minimum width dropdown list if all labels are short', async () => {
const { openCombobox } = renderFieldPicker();
openCombobox();
const popover = screen.getByRole('dialog');
expect(popover).toHaveStyle('inline-size: 256px');
});
it('should render calculated width dropdown list if the longest label is longer than min width', async () => {
const { openCombobox } = renderFieldPicker(generateFieldWithLabelOfLength(50));
openCombobox();
const popover = screen.getByRole('dialog');
expect(popover).toHaveStyle('inline-size: 466px');
});
it('should render maximum width dropdown list if the longest label is longer than max width', async () => {
const { openCombobox } = renderFieldPicker(generateFieldWithLabelOfLength(80));
openCombobox();
const popover = screen.getByRole('dialog');
expect(popover).toHaveStyle('inline-size: 550px');
});
});

View file

@ -9,9 +9,10 @@
import './field_picker.scss';
import React from 'react';
import { i18n } from '@kbn/i18n';
import classNames from 'classnames';
import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui';
import { FieldIcon } from '@kbn/field-utils/src/components/field_icon';
import classNames from 'classnames';
import { calculateWidthFromCharCount } from '@kbn/calculate-width-from-char-count';
import type { FieldOptionValue, FieldOption } from './types';
export interface FieldPickerProps<T extends FieldOptionValue>
@ -27,23 +28,26 @@ export interface FieldPickerProps<T extends FieldOptionValue>
const MIDDLE_TRUNCATION_PROPS = { truncation: 'middle' as const };
const SINGLE_SELECTION_AS_TEXT_PROPS = { asPlainText: true };
export function FieldPicker<T extends FieldOptionValue = FieldOptionValue>({
selectedOptions,
options,
onChoose,
onDelete,
fieldIsInvalid,
['data-test-subj']: dataTestSub,
...rest
}: FieldPickerProps<T>) {
let theLongestLabel = '';
export function FieldPicker<T extends FieldOptionValue = FieldOptionValue>(
props: FieldPickerProps<T>
) {
const {
selectedOptions,
options,
onChoose,
onDelete,
fieldIsInvalid,
['data-test-subj']: dataTestSub,
...rest
} = props;
let maxLabelLength = 0;
const styledOptions = options?.map(({ compatible, exists, ...otherAttr }) => {
if (otherAttr.options) {
return {
...otherAttr,
options: otherAttr.options.map(({ exists: fieldOptionExists, ...fieldOption }) => {
if (fieldOption.label.length > theLongestLabel.length) {
theLongestLabel = fieldOption.label;
if (fieldOption.label.length > maxLabelLength) {
maxLabelLength = fieldOption.label.length;
}
return {
...fieldOption,
@ -75,7 +79,6 @@ export function FieldPicker<T extends FieldOptionValue = FieldOptionValue>({
};
});
const panelMinWidth = getPanelMinWidth(theLongestLabel.length);
return (
<EuiComboBox
fullWidth
@ -90,7 +93,10 @@ export function FieldPicker<T extends FieldOptionValue = FieldOptionValue>({
selectedOptions={selectedOptions}
singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS}
truncationProps={MIDDLE_TRUNCATION_PROPS}
inputPopoverProps={{ panelMinWidth }}
inputPopoverProps={{
panelMinWidth: calculateWidthFromCharCount(maxLabelLength),
anchorPosition: 'downRight',
}}
onChange={(choices) => {
if (choices.length === 0) {
onDelete?.();
@ -102,20 +108,3 @@ export function FieldPicker<T extends FieldOptionValue = FieldOptionValue>({
/>
);
}
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;
}

View file

@ -31,5 +31,6 @@
"@kbn/coloring",
"@kbn/field-formats-plugin",
"@kbn/field-utils",
"@kbn/calculate-width-from-char-count"
],
}

View file

@ -9,6 +9,7 @@
import React, { useState } from 'react';
import { EuiSelectable, EuiInputPopover, EuiSelectableProps } from '@elastic/eui';
import { DataViewListItem } from '@kbn/data-views-plugin/common';
import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count';
import { ToolbarButton, ToolbarButtonProps } from '@kbn/shared-ux-button-toolbar';
@ -67,6 +68,7 @@ export function DataViewPicker({
isOpen={isPopoverOpen}
input={createTrigger()}
closePopover={() => setPopoverIsOpen(false)}
panelMinWidth={calculateWidthFromEntries(dataViews, ['name', 'id'])}
panelProps={{
'data-test-subj': 'data-view-picker-popover',
}}

View file

@ -31,7 +31,8 @@
"@kbn/ui-actions-plugin",
"@kbn/saved-objects-finder-plugin",
"@kbn/content-management-plugin",
"@kbn/shared-ux-button-toolbar"
"@kbn/shared-ux-button-toolbar",
"@kbn/calculate-width-from-char-count"
],
"exclude": ["target/**/*"]
}

View file

@ -9,6 +9,7 @@
import { EuiComboBox, EuiComboBoxOptionOption, EuiToolTip, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useState } from 'react';
import { UnifiedHistogramBreakdownContext } from '../types';
@ -59,11 +60,10 @@ export const BreakdownFieldSelector = ({
const breakdownCss = css`
width: 100%;
max-width: ${euiTheme.base * 22}px;
&:focus-within {
max-width: ${euiTheme.base * 30}px;
}
`;
const panelMinWidth = calculateWidthFromEntries(fieldOptions, ['label']);
return (
<EuiToolTip
position="top"
@ -81,6 +81,7 @@ export const BreakdownFieldSelector = ({
aria-label={i18n.translate('unifiedHistogram.breakdownFieldSelectorAriaLabel', {
defaultMessage: 'Break down by',
})}
inputPopoverProps={{ panelMinWidth, anchorPosition: 'downRight' }}
singleSelection={SINGLE_SELECTION}
options={fieldOptions}
selectedOptions={selectedFields}

View file

@ -26,6 +26,7 @@
"@kbn/visualizations-plugin",
"@kbn/discover-utils",
"@kbn/resizable-layout",
"@kbn/calculate-width-from-char-count",
],
"exclude": [
"target/**/*",

View file

@ -6,15 +6,24 @@
* Side Public License, v 1.
*/
export const DATA_VIEW_POPOVER_CONTENT_WIDTH = 280;
import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count';
import { DataViewListItemEnhanced } from './dataview_list';
export const changeDataViewStyles = ({ fullWidth }: { fullWidth?: boolean }) => {
const MIN_WIDTH = 300;
export const changeDataViewStyles = ({
fullWidth,
dataViewsList,
}: {
fullWidth?: boolean;
dataViewsList: DataViewListItemEnhanced[];
}) => {
return {
trigger: {
maxWidth: fullWidth ? undefined : DATA_VIEW_POPOVER_CONTENT_WIDTH,
maxWidth: fullWidth ? undefined : MIN_WIDTH,
},
popoverContent: {
width: DATA_VIEW_POPOVER_CONTENT_WIDTH,
width: calculateWidthFromEntries(dataViewsList, ['name', 'id'], { minWidth: MIN_WIDTH }),
},
};
};

View file

@ -96,7 +96,9 @@ export function ChangeDataView({
const { application, data, storage, dataViews, dataViewEditor, appName, usageCollection } =
kibana.services;
const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName);
const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth });
const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth, dataViewsList });
const [isTextLangTransitionModalDismissed, setIsTextLangTransitionModalDismissed] = useState(() =>
Boolean(storage.get(TEXT_LANG_TRANSITION_MODAL_KEY))
);

View file

@ -10,6 +10,7 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
import { uniq } from 'lodash';
import React from 'react';
import { withKibana } from '@kbn/kibana-react-plugin/public';
import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count';
import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box';
import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor';
import { ValueInputType } from './value_input_type';
@ -26,7 +27,6 @@ interface PhraseValueInputProps extends PhraseSuggestorProps {
}
class PhraseValueInputUI extends PhraseSuggestorUI<PhraseValueInputProps> {
comboBoxWrapperRef = React.createRef<HTMLDivElement>();
inputRef: HTMLInputElement | null = null;
public render() {
@ -59,43 +59,39 @@ class PhraseValueInputUI extends PhraseSuggestorUI<PhraseValueInputProps> {
// there are cases when the value is a number, this would cause an exception
const valueAsStr = String(value);
const options = value ? uniq([valueAsStr, ...suggestions]) : suggestions;
const panelMinWidth = calculateWidthFromEntries(options);
return (
<div ref={this.comboBoxWrapperRef}>
<StringComboBox
async
isLoading={isLoading}
inputRef={(ref) => {
this.inputRef = ref;
}}
isDisabled={this.props.disabled}
fullWidth={fullWidth}
compressed={this.props.compressed}
placeholder={intl.formatMessage({
id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder',
defaultMessage: 'Select a value',
})}
aria-label={intl.formatMessage({
id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder',
defaultMessage: 'Select a value',
})}
options={options}
getLabel={(option) => option}
selectedOptions={value ? [valueAsStr] : []}
onChange={([newValue = '']) => {
onChange(newValue);
setTimeout(() => {
// Note: requires a tick skip to correctly blur element focus
this.inputRef?.blur();
});
}}
onSearchChange={this.onSearchChange}
onCreateOption={onChange}
isClearable={false}
data-test-subj="filterParamsComboBox phraseParamsComboxBox"
singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS}
truncationProps={MIDDLE_TRUNCATION_PROPS}
/>
</div>
<StringComboBox
async
isLoading={isLoading}
inputRef={(ref) => {
this.inputRef = ref;
}}
isDisabled={this.props.disabled}
fullWidth={fullWidth}
compressed={this.props.compressed}
placeholder={intl.formatMessage({
id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder',
defaultMessage: 'Select a value',
})}
aria-label={intl.formatMessage({
id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder',
defaultMessage: 'Select a value',
})}
options={options}
getLabel={(option) => option}
selectedOptions={value ? [valueAsStr] : []}
onChange={([newValue = '']) => {
onChange(newValue);
}}
onSearchChange={this.onSearchChange}
onCreateOption={onChange}
isClearable={false}
data-test-subj="filterParamsComboBox phraseParamsComboxBox"
singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS}
truncationProps={MIDDLE_TRUNCATION_PROPS}
inputPopoverProps={{ panelMinWidth, anchorPosition: 'downRight' }}
/>
);
}
}

View file

@ -11,6 +11,7 @@ import { uniq } from 'lodash';
import React from 'react';
import { withKibana } from '@kbn/kibana-react-plugin/public';
import { withEuiTheme, WithEuiThemeProps } from '@elastic/eui';
import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count';
import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box';
import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor';
import { phrasesValuesComboboxCss } from './phrases_values_input.styles';
@ -28,45 +29,42 @@ interface Props {
export type PhrasesValuesInputProps = Props & PhraseSuggestorProps & WithEuiThemeProps;
class PhrasesValuesInputUI extends PhraseSuggestorUI<PhrasesValuesInputProps> {
comboBoxWrapperRef = React.createRef<HTMLDivElement>();
public render() {
const { suggestions, isLoading } = this.state;
const { values, intl, onChange, fullWidth, onParamsUpdate, compressed, disabled } = this.props;
const options = values ? uniq([...values, ...suggestions]) : suggestions;
const panelMinWidth = calculateWidthFromEntries(options);
return (
<div ref={this.comboBoxWrapperRef}>
<StringComboBox
async
isLoading={isLoading}
fullWidth={fullWidth}
compressed={compressed}
placeholder={intl.formatMessage({
id: 'unifiedSearch.filter.filterEditor.valuesSelectPlaceholder',
defaultMessage: 'Select values',
})}
aria-label={intl.formatMessage({
id: 'unifiedSearch.filter.filterEditor.valuesSelectPlaceholder',
defaultMessage: 'Select values',
})}
delimiter=","
isCaseSensitive={true}
options={options}
getLabel={(option) => option}
selectedOptions={values || []}
onSearchChange={this.onSearchChange}
onCreateOption={(option: string) => {
onParamsUpdate(option.trim());
}}
className={phrasesValuesComboboxCss(this.props.theme)}
onChange={onChange}
isClearable={false}
data-test-subj="filterParamsComboBox phrasesParamsComboxBox"
isDisabled={disabled}
truncationProps={MIDDLE_TRUNCATION_PROPS}
/>
</div>
<StringComboBox
async
isLoading={isLoading}
fullWidth={fullWidth}
compressed={compressed}
placeholder={intl.formatMessage({
id: 'unifiedSearch.filter.filterEditor.valuesSelectPlaceholder',
defaultMessage: 'Select values',
})}
aria-label={intl.formatMessage({
id: 'unifiedSearch.filter.filterEditor.valuesSelectPlaceholder',
defaultMessage: 'Select values',
})}
delimiter=","
isCaseSensitive={true}
options={options}
getLabel={(option) => option}
selectedOptions={values || []}
onSearchChange={this.onSearchChange}
onCreateOption={(option: string) => {
onParamsUpdate(option.trim());
}}
className={phrasesValuesComboboxCss(this.props.theme)}
onChange={onChange}
isClearable={false}
data-test-subj="filterParamsComboBox phrasesParamsComboxBox"
isDisabled={disabled}
truncationProps={MIDDLE_TRUNCATION_PROPS}
inputPopoverProps={{ panelMinWidth, anchorPosition: 'downRight' }}
/>
);
}
}

View file

@ -11,6 +11,7 @@ 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 { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count';
import { useGeneratedHtmlId, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { getFilterableFields } from '../../filter_bar/filter_editor';
import { FiltersBuilderContextType } from '../context';
@ -36,7 +37,6 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps)
const { disabled, suggestionsAbstraction } = useContext(FiltersBuilderContextType);
const fields = dataView ? getFilterableFields(dataView) : [];
const id = useGeneratedHtmlId({ prefix: 'fieldInput' });
const comboBoxWrapperRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const onFieldChange = useCallback(
@ -72,40 +72,30 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps)
({ label }) => fields[optionFields.findIndex((optionField) => optionField.label === label)]
);
onFieldChange(newValues);
setTimeout(() => {
// Note: requires a tick skip to correctly blur element focus
inputRef?.current?.blur();
});
};
const handleFocus: React.FocusEventHandler<HTMLDivElement> = () => {
// Force focus on input due to https://github.com/elastic/eui/issues/7170
inputRef?.current?.focus();
};
const panelMinWidth = calculateWidthFromEntries(euiOptions, ['label']);
return (
<div ref={comboBoxWrapperRef}>
<EuiComboBox
id={id}
inputRef={(ref) => {
inputRef.current = ref;
}}
options={euiOptions}
selectedOptions={selectedEuiOptions}
onChange={onComboBoxChange}
isDisabled={disabled}
placeholder={strings.getFieldSelectPlaceholderLabel()}
sortMatchesBy="startsWith"
aria-label={strings.getFieldSelectPlaceholderLabel()}
isClearable={false}
compressed
fullWidth
onFocus={handleFocus}
data-test-subj="filterFieldSuggestionList"
singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS}
truncationProps={MIDDLE_TRUNCATION_PROPS}
/>
</div>
<EuiComboBox
id={id}
inputRef={(ref) => {
inputRef.current = ref;
}}
options={euiOptions}
selectedOptions={selectedEuiOptions}
onChange={onComboBoxChange}
isDisabled={disabled}
placeholder={strings.getFieldSelectPlaceholderLabel()}
sortMatchesBy="startsWith"
aria-label={strings.getFieldSelectPlaceholderLabel()}
isClearable={false}
compressed
fullWidth
data-test-subj="filterFieldSuggestionList"
singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS}
truncationProps={MIDDLE_TRUNCATION_PROPS}
inputPopoverProps={{ panelMinWidth }}
/>
);
}

View file

@ -26,9 +26,6 @@ export const fieldAndParamCss = (euiTheme: EuiThemeComputed) => css`
.euiFormRow {
max-width: 800px;
}
&:focus-within {
flex-grow: 4;
}
`;
export const operationCss = (euiTheme: EuiThemeComputed) => css`

View file

@ -11,7 +11,9 @@ import React, { Component } from 'react';
import { Required } from '@kbn/utility-types';
import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui';
import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count';
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
import { MIDDLE_TRUNCATION_PROPS } from '../filter_bar/filter_editor/lib/helpers';
export type IndexPatternSelectProps = Required<
Omit<EuiComboBoxProps<any>, 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange'>,
@ -28,7 +30,7 @@ export type IndexPatternSelectInternalProps = IndexPatternSelectProps & {
interface IndexPatternSelectState {
isLoading: boolean;
options: [];
options: Array<{ value: string; label: string }>;
selectedIndexPattern: { value: string; label: string } | undefined;
searchValue: string | undefined;
}
@ -147,6 +149,8 @@ export default class IndexPatternSelect extends Component<IndexPatternSelectInte
...rest
} = this.props;
const panelMinWidth = calculateWidthFromEntries(this.state.options, ['label']);
return (
<EuiComboBox
{...rest}
@ -157,6 +161,8 @@ export default class IndexPatternSelect extends Component<IndexPatternSelectInte
options={this.state.options}
selectedOptions={this.state.selectedIndexPattern ? [this.state.selectedIndexPattern] : []}
onChange={this.onChange}
truncationProps={MIDDLE_TRUNCATION_PROPS}
inputPopoverProps={{ panelMinWidth }}
/>
);
}

View file

@ -42,6 +42,7 @@
"@kbn/core-doc-links-browser",
"@kbn/core-lifecycle-browser",
"@kbn/ml-string-hash",
"@kbn/calculate-width-from-char-count"
],
"exclude": [
"target/**/*",

View file

@ -104,6 +104,8 @@
"@kbn/bfetch-plugin/*": ["src/plugins/bfetch/*"],
"@kbn/calculate-auto": ["packages/kbn-calculate-auto"],
"@kbn/calculate-auto/*": ["packages/kbn-calculate-auto/*"],
"@kbn/calculate-width-from-char-count": ["packages/kbn-calculate-width-from-char-count"],
"@kbn/calculate-width-from-char-count/*": ["packages/kbn-calculate-width-from-char-count/*"],
"@kbn/canvas-plugin": ["x-pack/plugins/canvas"],
"@kbn/canvas-plugin/*": ["x-pack/plugins/canvas/*"],
"@kbn/cases-api-integration-test-plugin": ["x-pack/test/cases_api_integration/common/plugins/cases"],

View file

@ -6,9 +6,11 @@
*/
import { i18n } from '@kbn/i18n';
import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count';
import React, { useState } from 'react';
import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui';
import { DataViewsList } from '@kbn/unified-search-plugin/public';
import { css } from '@emotion/react';
import { type IndexPatternRef } from '../../types';
import { type ChangeIndexPatternTriggerProps, TriggerButton } from './trigger';
@ -30,43 +32,47 @@ export function ChangeIndexPattern({
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
return (
<>
<EuiPopover
panelClassName="lnsChangeIndexPatternPopover"
button={
<TriggerButton
{...trigger}
isMissingCurrent={isMissingCurrent}
togglePopover={() => setPopoverIsOpen(!isPopoverOpen)}
/>
}
panelProps={{
['data-test-subj']: 'lnsChangeIndexPatternPopover',
}}
isOpen={isPopoverOpen}
closePopover={() => setPopoverIsOpen(false)}
display="block"
panelPaddingSize="none"
ownFocus
<EuiPopover
button={
<TriggerButton
{...trigger}
isMissingCurrent={isMissingCurrent}
togglePopover={() => setPopoverIsOpen(!isPopoverOpen)}
/>
}
panelProps={{
['data-test-subj']: 'lnsChangeIndexPatternPopover',
}}
isOpen={isPopoverOpen}
closePopover={() => setPopoverIsOpen(false)}
display="block"
panelPaddingSize="none"
ownFocus
>
<div
css={css`
width: ${calculateWidthFromEntries(indexPatternRefs, ['name', 'id'], {
minWidth: 320,
maxWidth: 600,
})}px;
`}
>
<div>
<EuiPopoverTitle paddingSize="s">
{i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', {
defaultMessage: 'Data view',
})}
</EuiPopoverTitle>
<EuiPopoverTitle paddingSize="s">
{i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', {
defaultMessage: 'Data view',
})}
</EuiPopoverTitle>
<DataViewsList
dataViewsList={indexPatternRefs}
onChangeDataView={(newId) => {
onChangeIndexPattern(newId);
setPopoverIsOpen(false);
}}
currentDataViewId={indexPatternId}
selectableProps={selectableProps}
/>
</div>
</EuiPopover>
</>
<DataViewsList
dataViewsList={indexPatternRefs}
onChangeDataView={(newId) => {
onChangeIndexPattern(newId);
setPopoverIsOpen(false);
}}
currentDataViewId={indexPatternId}
selectableProps={selectableProps}
/>
</div>
</EuiPopover>
);
}

View file

@ -170,7 +170,6 @@ function DataLayerHeader(props: VisualizationLayerWidgetProps<State>) {
return (
<EuiPopover
panelClassName="lnsChangeIndexPatternPopover"
button={
<DataLayerHeaderTrigger
onClick={() => setPopoverIsOpen(!isPopoverOpen)}
@ -188,7 +187,11 @@ function DataLayerHeader(props: VisualizationLayerWidgetProps<State>) {
defaultMessage: 'Layer visualization type',
})}
</EuiPopoverTitle>
<div>
<div
css={css`
width: 320px;
`}
>
<EuiSelectable<{
key?: string;
label: string;

View file

@ -92,9 +92,10 @@
"@kbn/core-plugins-server",
"@kbn/text-based-languages",
"@kbn/field-utils",
"@kbn/discover-utils",
"@kbn/shared-ux-button-toolbar",
"@kbn/cell-actions",
"@kbn/shared-ux-button-toolbar"
"@kbn/calculate-width-from-char-count",
"@kbn/discover-utils"
],
"exclude": [
"target/**/*",

View file

@ -3145,6 +3145,10 @@
version "0.0.0"
uid ""
"@kbn/calculate-width-from-char-count@link:packages/kbn-calculate-width-from-char-count":
version "0.0.0"
uid ""
"@kbn/canvas-plugin@link:x-pack/plugins/canvas":
version "0.0.0"
uid ""