[Unified field list] debounce search (#187143)

## Summary

Updates to unified field list on typing are debounced - this way we
don't get so many updates when typing in the search input.

Flaky test runner:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6424

## Performance comparison
Test: typing the string: activem for metricbeat data (~6000 fields)

before (costly update on every keystroke):
<img width="669" alt="Screenshot 2024-06-28 at 17 28 38"
src="7075f7bc-2d90-4177-acac-69ac101b2ef1">

after (only one costly update when user stops typing):
<img width="269" alt="Screenshot 2024-06-28 at 17 24 43"
src="8c0ce4a3-7c1a-428b-a482-f6b4d87911e0">
This commit is contained in:
Marta Bondyra 2024-07-06 14:47:26 +02:00 committed by GitHub
parent 4504088b9a
commit e6f17e7c06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 164 additions and 80 deletions

View file

@ -6,10 +6,10 @@
* Side Public License, v 1.
*/
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { act } from 'react-dom/test-utils';
import React, { useState } from 'react';
import userEvent from '@testing-library/user-event';
import { FieldNameSearch, type FieldNameSearchProps } from './field_name_search';
import { render, screen, waitFor } from '@testing-library/react';
describe('UnifiedFieldList <FieldNameSearch />', () => {
it('should render correctly', async () => {
@ -19,35 +19,34 @@ describe('UnifiedFieldList <FieldNameSearch />', () => {
screenReaderDescriptionId: 'htmlId',
'data-test-subj': 'searchInput',
};
const wrapper = mountWithIntl(<FieldNameSearch {...props} />);
expect(wrapper.find('input').prop('aria-describedby')).toBe('htmlId');
act(() => {
wrapper.find('input').simulate('change', {
target: { value: 'hi' },
});
});
expect(props.onChange).toBeCalledWith('hi');
render(<FieldNameSearch {...props} />);
const input = screen.getByRole('searchbox', { name: 'Search field names' });
expect(input).toHaveAttribute('aria-describedby', 'htmlId');
userEvent.type(input, 'hey');
await waitFor(() => expect(props.onChange).toHaveBeenCalledWith('hey'), { timeout: 256 });
expect(props.onChange).toBeCalledTimes(1);
});
it('should update correctly', async () => {
const props: FieldNameSearchProps = {
nameFilter: 'this',
onChange: jest.fn(),
screenReaderDescriptionId: 'htmlId',
'data-test-subj': 'searchInput',
it('should accept the updates from the top', async () => {
const FieldNameSearchWithWrapper = ({ defaultNameFilter = '' }) => {
const [nameFilter, setNameFilter] = useState(defaultNameFilter);
const props: FieldNameSearchProps = {
nameFilter,
onChange: jest.fn(),
screenReaderDescriptionId: 'htmlId',
'data-test-subj': 'searchInput',
};
return (
<div>
<button onClick={() => setNameFilter('that')}>update nameFilter</button>
<FieldNameSearch {...props} />
</div>
);
};
const wrapper = mountWithIntl(<FieldNameSearch {...props} />);
expect(wrapper.find('input').prop('value')).toBe('this');
wrapper.setProps({
nameFilter: 'that',
});
expect(wrapper.find('input').prop('value')).toBe('that');
expect(props.onChange).not.toBeCalled();
render(<FieldNameSearchWithWrapper defaultNameFilter="this" />);
expect(screen.getByRole('searchbox')).toHaveValue('this');
const button = screen.getByRole('button', { name: 'update nameFilter' });
userEvent.click(button);
expect(screen.getByRole('searchbox')).toHaveValue('that');
});
});

View file

@ -9,6 +9,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFieldSearch, type EuiFieldSearchProps } from '@elastic/eui';
import { useDebouncedValue } from '@kbn/visualization-utils';
/**
* Props for FieldNameSearch component
@ -45,15 +46,22 @@ export const FieldNameSearch: React.FC<FieldNameSearchProps> = ({
description: 'Search the list of fields in the data view for the provided text',
});
const { inputValue, handleInputChange } = useDebouncedValue({
onChange,
value: nameFilter,
});
return (
<EuiFieldSearch
aria-describedby={screenReaderDescriptionId}
aria-label={searchPlaceholder}
data-test-subj={`${dataTestSubject}FieldSearch`}
fullWidth
onChange={(event) => onChange(event.target.value)}
onChange={(e) => {
handleInputChange(e.target.value);
}}
placeholder={searchPlaceholder}
value={nameFilter}
value={inputValue}
append={append}
compressed={compressed}
/>

View file

@ -22,6 +22,15 @@ import { FieldsAccordion } from './fields_accordion';
import { NoFieldsCallout } from './no_fields_callout';
import { useGroupedFields, type GroupedFieldsParams } from '../../hooks/use_grouped_fields';
jest.mock('lodash', () => {
const original = jest.requireActual('lodash');
return {
...original,
debounce: (fn: unknown) => fn,
};
});
describe('UnifiedFieldList FieldListGrouped + useGroupedFields()', () => {
let defaultProps: FieldListGroupedProps<DataViewField>;
let mockedServices: GroupedFieldsParams<DataViewField>['services'];

View file

@ -8,7 +8,7 @@
import React from 'react';
import { EuiFieldText, EuiFieldTextProps } from '@elastic/eui';
import { useDebouncedValue } from './debounced_value';
import { useDebouncedValue } from '@kbn/visualization-utils';
type Props = {
value: string;

View file

@ -12,8 +12,6 @@ export * from './name_input';
export * from './debounced_input';
export * from './debounced_value';
export * from './color_picker';
export * from './icon_select';

View file

@ -21,7 +21,7 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import type { NotificationsStart } from '@kbn/core-notifications-browser';
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import { useDebouncedValue } from '../debounced_value';
import { useDebouncedValue } from '@kbn/visualization-utils';
export interface QueryInputServices {
http: HttpStart;

View file

@ -10,7 +10,6 @@ export {
FieldPicker,
NameInput,
DebouncedInput,
useDebouncedValue,
ColorPicker,
IconSelect,
IconSelectSetting,

View file

@ -9,3 +9,4 @@
export { getTimeZone } from './src/get_timezone';
export { getLensAttributesFromSuggestion } from './src/get_lens_attributes';
export { TooltipWrapper } from './src/tooltip_wrapper';
export { useDebouncedValue } from './src/debounced_value';

View file

@ -9,16 +9,15 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { useDebouncedValue } from './debounced_value';
jest.mock('lodash', () => {
const original = jest.requireActual('lodash');
return {
...original,
debounce: (fn: unknown) => fn,
};
});
describe('useDebouncedValue', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
it('should update upstream value changes', () => {
const onChangeMock = jest.fn();
const { result } = renderHook(() => useDebouncedValue({ value: 'a', onChange: onChangeMock }));
@ -26,6 +25,8 @@ describe('useDebouncedValue', () => {
act(() => {
result.current.handleInputChange('b');
});
expect(onChangeMock).not.toHaveBeenCalled();
jest.advanceTimersByTime(256);
expect(onChangeMock).toHaveBeenCalledWith('b');
});
@ -37,7 +38,8 @@ describe('useDebouncedValue', () => {
act(() => {
result.current.handleInputChange('');
});
expect(onChangeMock).not.toHaveBeenCalled();
jest.advanceTimersByTime(256);
expect(onChangeMock).toHaveBeenCalledWith('a');
});
@ -50,7 +52,23 @@ describe('useDebouncedValue', () => {
act(() => {
result.current.handleInputChange('');
});
expect(onChangeMock).not.toHaveBeenCalled();
jest.advanceTimersByTime(256);
expect(onChangeMock).toHaveBeenCalledWith('');
});
it('custom wait time is respected', () => {
const onChangeMock = jest.fn();
const { result } = renderHook(() =>
useDebouncedValue({ value: 'a', onChange: onChangeMock }, { wait: 500 })
);
act(() => {
result.current.handleInputChange('b');
});
expect(onChangeMock).not.toHaveBeenCalled();
jest.advanceTimersByTime(256);
expect(onChangeMock).not.toHaveBeenCalled();
jest.advanceTimersByTime(244); // sums to 500
expect(onChangeMock).toHaveBeenCalledWith('b');
});
});

View file

@ -9,13 +9,13 @@
import { useState, useMemo, useEffect, useRef } from 'react';
import { debounce } from 'lodash';
const DEFAULT_TIMEOUT = 256;
/**
* Debounces value changes and updates inputValue on root state changes if no debounced changes
* are in flight because the user is currently modifying the value.
*
* * allowFalsyValue: update upstream with all falsy values but null or undefined
*
* When testing this function mock the "debounce" function in lodash (see this module test for an example)
* * wait: debounce timeout
*/
export const useDebouncedValue = <T>(
@ -28,7 +28,9 @@ export const useDebouncedValue = <T>(
value: T;
defaultValue?: T;
},
{ allowFalsyValue }: { allowFalsyValue?: boolean } = {}
{ allowFalsyValue, wait = DEFAULT_TIMEOUT }: { allowFalsyValue?: boolean; wait?: number } = {
wait: DEFAULT_TIMEOUT,
}
) => {
const [inputValue, setInputValue] = useState(value);
const unflushedChanges = useRef(false);
@ -45,8 +47,8 @@ export const useDebouncedValue = <T>(
// do not reset unflushed flag right away, wait a bit for upstream to pick it up
flushChangesTimeout.current = setTimeout(() => {
unflushedChanges.current = false;
}, 256);
}, 256);
}, wait);
}, wait);
return (val: T) => {
if (flushChangesTimeout.current) {
clearTimeout(flushChangesTimeout.current);
@ -54,7 +56,7 @@ export const useDebouncedValue = <T>(
unflushedChanges.current = true;
callback(val);
};
}, [onChange]);
}, [onChange, wait]);
useEffect(() => {
if (!unflushedChanges.current && value !== inputValue) {

View file

@ -66,6 +66,15 @@ jest.mock('../../../../customizations', () => ({
}),
}));
jest.mock('lodash', () => {
const original = jest.requireActual('lodash');
return {
...original,
debounce: (fn: unknown) => fn,
};
});
jest.mock('@kbn/unified-field-list/src/services/field_stats', () => ({
loadFieldStats: jest.fn().mockResolvedValue({
totalDocuments: 1624,

View file

@ -230,7 +230,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('add and remove columns', async function () {
const extraColumns = ['phpmemory', 'ip'];
const expectedFieldLength: Record<string, number> = {
phpmemory: 1,
ip: 4,
};
afterEach(async function () {
for (const column of extraColumns) {
await PageObjects.unifiedFieldList.clickFieldListItemRemove(column);
@ -242,6 +245,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
for (const column of extraColumns) {
await PageObjects.unifiedFieldList.clearFieldSearchInput();
await PageObjects.unifiedFieldList.findFieldByName(column);
await PageObjects.unifiedFieldList.waitUntilFieldlistHasCountOfFields(
expectedFieldLength[column]
);
await retry.waitFor('field to appear', async function () {
return await testSubjects.exists(`field-${column}`);
});
@ -258,9 +264,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
for (const column of extraColumns) {
await PageObjects.unifiedFieldList.clearFieldSearchInput();
await PageObjects.unifiedFieldList.findFieldByName(column);
await retry.waitFor('field to appear', async function () {
return await testSubjects.exists(`field-${column}`);
});
await PageObjects.unifiedFieldList.waitUntilFieldlistHasCountOfFields(
expectedFieldLength[column]
);
await PageObjects.unifiedFieldList.clickFieldListItemAdd(column);
await PageObjects.header.waitUntilLoadingHasFinished();
}

View file

@ -220,6 +220,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('add and remove columns', function () {
const extraColumns = ['phpmemory', 'ip'];
const expectedFieldLength: Record<string, number> = {
phpmemory: 1,
ip: 4,
};
afterEach(async function () {
for (const column of extraColumns) {
@ -232,6 +236,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
for (const column of extraColumns) {
await PageObjects.unifiedFieldList.clearFieldSearchInput();
await PageObjects.unifiedFieldList.findFieldByName(column);
await PageObjects.unifiedFieldList.waitUntilFieldlistHasCountOfFields(
expectedFieldLength[column]
);
await PageObjects.unifiedFieldList.clickFieldListItemAdd(column);
await PageObjects.header.waitUntilLoadingHasFinished();
// test the header now
@ -244,6 +251,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
for (const column of extraColumns) {
await PageObjects.unifiedFieldList.clearFieldSearchInput();
await PageObjects.unifiedFieldList.findFieldByName(column);
await PageObjects.unifiedFieldList.waitUntilFieldlistHasCountOfFields(
expectedFieldLength[column]
);
await PageObjects.unifiedFieldList.clickFieldListItemAdd(column);
await PageObjects.header.waitUntilLoadingHasFinished();
}

View file

@ -53,6 +53,15 @@ export class UnifiedFieldListPageObject extends FtrService {
});
}
public async waitUntilFieldlistHasCountOfFields(count: number) {
await this.retry.waitFor('wait until fieldlist has updated number of fields', async () => {
return (
(await this.find.allByCssSelector('#fieldListGroupedAvailableFields .kbnFieldButton'))
.length === count
);
});
}
public async doesSidebarShowFields() {
return await this.testSubjects.exists('fieldListGroupedFieldGroups');
}

View file

@ -18,7 +18,7 @@ import {
EuiLink,
useEuiTheme,
} from '@elastic/eui';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
DEFAULT_DURATION_INPUT_FORMAT,

View file

@ -15,7 +15,7 @@ import {
ExpressionAstExpressionBuilder,
ExpressionAstFunctionBuilder,
} from '@kbn/expressions-plugin/public';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { PERCENTILE_ID, PERCENTILE_NAME } from '@kbn/lens-formula-docs';
import { OperationDefinition } from '.';
import {

View file

@ -10,7 +10,7 @@ import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { AggFunctionsMapping } from '@kbn/data-plugin/public';
import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { PERCENTILE_RANK_ID, PERCENTILE_RANK_NAME } from '@kbn/lens-formula-docs';
import { OperationDefinition } from '.';
import {

View file

@ -8,7 +8,7 @@
import React, { useRef } from 'react';
import { EuiFieldText, keys } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
export const LabelInput = ({
value,

View file

@ -7,7 +7,7 @@
import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFieldNumber, EuiFormRow } from '@elastic/eui';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { OperationDefinition } from '.';
import {
ReferenceBasedIndexPatternColumn,

View file

@ -9,12 +9,12 @@ import React, { useCallback, useMemo } from 'react';
import { htmlIdGenerator } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
useDebouncedValue,
DragDropBuckets,
FieldsBucketContainer,
NewBucketButton,
DraggableBucketContainer,
} from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { IndexPattern } from '../../../../../types';
import { FieldSelect } from '../../../dimension_panel/field_select';
import type { TermsIndexPatternColumn } from './types';

View file

@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import { uniq } from 'lodash';
import { EuiComboBox, EuiFormRow, EuiSpacer, EuiSwitch, EuiFieldText, EuiText } from '@elastic/eui';
import type { DatatableRow } from '@kbn/expressions-plugin/common';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
export interface IncludeExcludeOptions {
label: string;

View file

@ -8,7 +8,7 @@
import React, { useCallback, useMemo } from 'react';
import { EuiSpacer, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import type { AxesSettingsConfig } from '../../../visualizations/xy/types';
import { type LabelMode, VisLabel } from '../..';

View file

@ -8,7 +8,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFieldNumber, EuiFormRow } from '@elastic/eui';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
export const DEFAULT_FLOATING_COLUMNS = 1;

View file

@ -20,7 +20,7 @@ import {
} from '@elastic/eui';
import { Position, VerticalAlignment, HorizontalAlignment, LegendValue } from '@elastic/charts';
import { LegendSize } from '@kbn/visualizations-plugin/public';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { XYLegendValue } from '@kbn/visualizations-plugin/common/constants';
import { ToolbarPopover, type ToolbarPopoverProps } from '../toolbar_popover';
import { LegendLocationSettings } from './location/legend_location_settings';

View file

@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { EuiFormRow, EuiFieldText, EuiText, useEuiTheme, EuiComboBox } from '@elastic/eui';
import { PaletteRegistry } from '@kbn/coloring';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import type { VisualizationDimensionEditorProps } from '../../../types';
import type { DatatableVisualizationState } from '../visualization';

View file

@ -9,7 +9,7 @@ import React, { memo, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { GaugeLabelMajorMode } from '@kbn/expression-gauge-plugin/common';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import type { VisualizationToolbarProps } from '../../../types';
import { ToolbarPopover, VisLabel } from '../../../shared_components';
import './gauge_config_panel.scss';

View file

@ -28,7 +28,8 @@ import {
import { getDataBoundsForPalette } from '@kbn/expression-metric-vis-plugin/public';
import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils';
import { css } from '@emotion/react';
import { DebouncedInput, useDebouncedValue, IconSelect } from '@kbn/visualization-ui-components';
import { DebouncedInput, IconSelect } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils';
import { applyPaletteParams, PalettePanelContainer } from '../../shared_components';
import type { VisualizationDimensionEditorProps } from '../../types';

View file

@ -8,7 +8,7 @@
import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFormRow, EuiFieldText } from '@elastic/eui';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { VisualizationToolbarProps } from '../../types';
import { ToolbarPopover } from '../../shared_components';
import { MetricVisualizationState } from './visualization';

View file

@ -17,7 +17,8 @@ import {
AVAILABLE_PALETTES,
getColorsFromMapping,
} from '@kbn/coloring';
import { ColorPicker, useDebouncedValue } from '@kbn/visualization-ui-components';
import { ColorPicker } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { EuiFormRow, EuiFlexGroup, EuiFlexItem, EuiSwitch, EuiText, EuiBadge } from '@elastic/eui';
import { useState, useCallback } from 'react';
import { getColorCategories } from '@kbn/chart-expressions-common';

View file

@ -18,7 +18,7 @@ import {
} from '@elastic/eui';
import { LegendValue, Position } from '@elastic/charts';
import { LegendSize } from '@kbn/visualizations-plugin/public';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { type PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants';
import { DEFAULT_PERCENT_DECIMALS } from './constants';
import { PartitionChartsMeta } from './partition_charts_meta';

View file

@ -19,7 +19,7 @@ import {
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiSwitch, EuiFormRow, EuiText, EuiBadge } from '@elastic/eui';
import { useState, MutableRefObject, useCallback } from 'react';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { getColorCategories } from '@kbn/chart-expressions-common';
import type { TagcloudState } from './types';
import { PalettePanelContainer, PalettePicker } from '../../shared_components';

View file

@ -17,7 +17,7 @@ import {
EuiIconAxisRight,
EuiIconAxisTop,
} from '@kbn/chart-icons';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { isHorizontalChart } from '../state_helpers';
import {
ToolbarPopover,

View file

@ -7,7 +7,7 @@
import React, { useCallback, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { ColorPicker } from '@kbn/visualization-ui-components';
import {

View file

@ -11,12 +11,12 @@ import { EuiButtonGroup, EuiFormRow } from '@elastic/eui';
import type { PaletteRegistry } from '@kbn/coloring';
import { FillStyle } from '@kbn/expression-xy-plugin/common';
import {
useDebouncedValue,
IconSelectSetting,
ColorPicker,
LineStyleSettings,
TextDecorationSetting,
} from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import type { VisualizationDimensionEditorProps } from '../../../../types';
import { State, XYState, XYReferenceLineLayerConfig, YConfig } from '../../types';
import { FormatFactory } from '../../../../../common/types';

View file

@ -8,7 +8,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiRange } from '@elastic/eui';
import { useDebouncedValue } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
export interface FillOpacityOptionProps {
/**

View file

@ -125,6 +125,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should show stats for a numeric runtime field', async () => {
await PageObjects.lens.searchField('runtime');
await PageObjects.lens.waitForMissingField('Records');
await PageObjects.lens.waitForField('runtime_number');
const [fieldId] = await PageObjects.lens.findFieldIdsByType('number');
await log.debug(`Opening field stats for ${fieldId}`);

View file

@ -319,6 +319,10 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await testSubjects.existOrFail(`lnsFieldListPanelField-${field}`);
},
async waitForMissingField(field: string) {
await testSubjects.missingOrFail(`lnsFieldListPanelField-${field}`);
},
async waitForMissingDataViewWarning() {
await retry.try(async () => {
await testSubjects.existOrFail(`missing-refs-failure`);

View file

@ -221,7 +221,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('add and remove columns', function () {
const extraColumns = ['phpmemory', 'ip'];
const expectedFieldLength: Record<string, number> = {
phpmemory: 1,
ip: 4,
};
afterEach(async function () {
for (const column of extraColumns) {
await PageObjects.unifiedFieldList.clickFieldListItemRemove(column);
@ -233,6 +236,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
for (const column of extraColumns) {
await PageObjects.unifiedFieldList.clearFieldSearchInput();
await PageObjects.unifiedFieldList.findFieldByName(column);
await PageObjects.unifiedFieldList.waitUntilFieldlistHasCountOfFields(
expectedFieldLength[column]
);
await PageObjects.unifiedFieldList.clickFieldListItemAdd(column);
await PageObjects.header.waitUntilLoadingHasFinished();
// test the header now
@ -245,6 +251,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
for (const column of extraColumns) {
await PageObjects.unifiedFieldList.clearFieldSearchInput();
await PageObjects.unifiedFieldList.findFieldByName(column);
await PageObjects.unifiedFieldList.waitUntilFieldlistHasCountOfFields(
expectedFieldLength[column]
);
await PageObjects.unifiedFieldList.clickFieldListItemAdd(column);
await PageObjects.header.waitUntilLoadingHasFinished();
}