[Lens] Avoid unnecessary data fetching on dimension flyout open (#82957)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marco Liberati 2020-11-16 14:35:12 +01:00 committed by GitHub
parent 01b1710eb7
commit c8b8a0ae9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 130 additions and 64 deletions

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useRef } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
export const useDebounceWithOptions = (
fn: Function,
{ skipFirstRender }: { skipFirstRender: boolean } = { skipFirstRender: false },
ms?: number | undefined,
deps?: React.DependencyList | undefined
) => {
const isFirstRender = useRef(true);
const newDeps = [...(deps || []), isFirstRender];
return useDebounce(
() => {
if (skipFirstRender && isFirstRender.current) {
isFirstRender.current = false;
return;
}
return fn();
},
ms,
newDeps
);
};

View file

@ -8,7 +8,6 @@ import './advanced_editor.scss';
import React, { useState, MouseEventHandler } from 'react';
import { i18n } from '@kbn/i18n';
import useDebounce from 'react-use/lib/useDebounce';
import {
EuiFlexGroup,
EuiFlexItem,
@ -31,6 +30,7 @@ import {
DraggableBucketContainer,
LabelInput,
} from '../shared_components';
import { useDebounceWithOptions } from '../helpers';
const generateId = htmlIdGenerator();
@ -208,12 +208,13 @@ export const AdvancedRangeEditor = ({
const lastIndex = localRanges.length - 1;
// Update locally all the time, but bounce the parents prop function
// to aviod too many requests
useDebounce(
// Update locally all the time, but bounce the parents prop function to aviod too many requests
// Avoid to trigger on first render
useDebounceWithOptions(
() => {
setRanges(localRanges.map(({ id, ...rest }) => ({ ...rest })));
},
{ skipFirstRender: true },
TYPING_DEBOUNCE_TIME,
[localRanges]
);

View file

@ -6,7 +6,6 @@
import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import useDebounce from 'react-use/lib/useDebounce';
import {
EuiButtonEmpty,
EuiFormRow,
@ -21,6 +20,7 @@ import { IFieldFormat } from 'src/plugins/data/public';
import { RangeColumnParams, UpdateParamsFnType, MODES_TYPES } from './ranges';
import { AdvancedRangeEditor } from './advanced_editor';
import { TYPING_DEBOUNCE_TIME, MODES, MIN_HISTOGRAM_BARS } from './constants';
import { useDebounceWithOptions } from '../helpers';
const BaseRangeEditor = ({
maxBars,
@ -37,10 +37,11 @@ const BaseRangeEditor = ({
}) => {
const [maxBarsValue, setMaxBarsValue] = useState(String(maxBars));
useDebounce(
useDebounceWithOptions(
() => {
onMaxBarsChange(Number(maxBarsValue));
},
{ skipFirstRender: true },
TYPING_DEBOUNCE_TIME,
[maxBarsValue]
);
@ -151,13 +152,14 @@ export const RangeEditor = ({
}) => {
const [isAdvancedEditor, toggleAdvancedEditor] = useState(params.type === MODES.Range);
// if the maxBars in the params is set to auto refresh it with the default value
// only on bootstrap
// if the maxBars in the params is set to auto refresh it with the default value only on bootstrap
useEffect(() => {
if (params.maxBars !== maxBars) {
setParam('maxBars', maxBars);
if (!isAdvancedEditor) {
if (params.maxBars !== maxBars) {
setParam('maxBars', maxBars);
}
}
}, [maxBars, params.maxBars, setParam]);
}, [maxBars, params.maxBars, setParam, isAdvancedEditor]);
if (isAdvancedEditor) {
return (

View file

@ -284,7 +284,11 @@ describe('ranges', () => {
/>
);
// There's a useEffect in the component that updates the value on bootstrap
// because there's a debouncer, wait a bit before calling onChange
act(() => {
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
instance.find(EuiRange).prop('onChange')!(
{
currentTarget: {
@ -293,26 +297,27 @@ describe('ranges', () => {
} as React.ChangeEvent<HTMLInputElement>,
true
);
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
expect(setStateSpy).toHaveBeenCalledWith({
...state,
layers: {
first: {
...state.layers.first,
columns: {
...state.layers.first.columns,
col1: {
...state.layers.first.columns.col1,
params: {
...state.layers.first.columns.col1.params,
maxBars: MAX_HISTOGRAM_VALUE,
},
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
});
expect(setStateSpy).toHaveBeenCalledWith({
...state,
layers: {
first: {
...state.layers.first,
columns: {
...state.layers.first.columns,
col1: {
...state.layers.first.columns.col1,
params: {
...state.layers.first.columns.col1.params,
maxBars: MAX_HISTOGRAM_VALUE,
},
},
},
},
});
},
});
});
@ -330,59 +335,65 @@ describe('ranges', () => {
/>
);
// There's a useEffect in the component that updates the value on bootstrap
// because there's a debouncer, wait a bit before calling onChange
act(() => {
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
// minus button
instance
.find('[data-test-subj="lns-indexPattern-range-maxBars-minus"]')
.find('button')
.prop('onClick')!({} as ReactMouseEvent);
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
});
expect(setStateSpy).toHaveBeenCalledWith({
...state,
layers: {
first: {
...state.layers.first,
columns: {
...state.layers.first.columns,
col1: {
...state.layers.first.columns.col1,
params: {
...state.layers.first.columns.col1.params,
maxBars: GRANULARITY_DEFAULT_VALUE - GRANULARITY_STEP,
},
expect(setStateSpy).toHaveBeenCalledWith({
...state,
layers: {
first: {
...state.layers.first,
columns: {
...state.layers.first.columns,
col1: {
...state.layers.first.columns.col1,
params: {
...state.layers.first.columns.col1.params,
maxBars: GRANULARITY_DEFAULT_VALUE - GRANULARITY_STEP,
},
},
},
},
});
},
});
act(() => {
// plus button
instance
.find('[data-test-subj="lns-indexPattern-range-maxBars-plus"]')
.find('button')
.prop('onClick')!({} as ReactMouseEvent);
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
});
expect(setStateSpy).toHaveBeenCalledWith({
...state,
layers: {
first: {
...state.layers.first,
columns: {
...state.layers.first.columns,
col1: {
...state.layers.first.columns.col1,
params: {
...state.layers.first.columns.col1.params,
maxBars: GRANULARITY_DEFAULT_VALUE,
},
expect(setStateSpy).toHaveBeenCalledWith({
...state,
layers: {
first: {
...state.layers.first,
columns: {
...state.layers.first.columns,
col1: {
...state.layers.first.columns.col1,
params: {
...state.layers.first.columns.col1.params,
maxBars: GRANULARITY_DEFAULT_VALUE,
},
},
},
},
});
},
});
// });
});
});
@ -749,6 +760,22 @@ describe('ranges', () => {
);
});
it('should not update the state on mount', () => {
const setStateSpy = jest.fn();
mount(
<InlineOptions
{...defaultOptions}
state={state}
setState={setStateSpy}
columnId="col1"
currentColumn={state.layers.first.columns.col1 as RangeIndexPatternColumn}
layerId="first"
/>
);
expect(setStateSpy.mock.calls.length).toBe(0);
});
it('should not reset formatters when switching between custom ranges and auto histogram', () => {
const setStateSpy = jest.fn();
// now set a format on the range operation

View file

@ -20,6 +20,13 @@ describe('ValuesRangeInput', () => {
expect(instance.find(EuiRange).prop('value')).toEqual('5');
});
it('should not run onChange function on mount', () => {
const onChangeSpy = jest.fn();
shallow(<ValuesRangeInput value={5} onChange={onChangeSpy} />);
expect(onChangeSpy.mock.calls.length).toBe(0);
});
it('should run onChange function on update', () => {
const onChangeSpy = jest.fn();
const instance = shallow(<ValuesRangeInput value={5} onChange={onChangeSpy} />);
@ -30,11 +37,10 @@ describe('ValuesRangeInput', () => {
);
});
expect(instance.find(EuiRange).prop('value')).toEqual('7');
// useDebounce runs on initialization and on change
expect(onChangeSpy.mock.calls.length).toBe(2);
expect(onChangeSpy.mock.calls[0][0]).toBe(5);
expect(onChangeSpy.mock.calls[1][0]).toBe(7);
expect(onChangeSpy.mock.calls.length).toBe(1);
expect(onChangeSpy.mock.calls[0][0]).toBe(7);
});
it('should not run onChange function on update when value is out of 1-100 range', () => {
const onChangeSpy = jest.fn();
const instance = shallow(<ValuesRangeInput value={5} onChange={onChangeSpy} />);
@ -46,9 +52,7 @@ describe('ValuesRangeInput', () => {
});
instance.update();
expect(instance.find(EuiRange).prop('value')).toEqual('107');
// useDebounce only runs on initialization
expect(onChangeSpy.mock.calls.length).toBe(2);
expect(onChangeSpy.mock.calls[0][0]).toBe(5);
expect(onChangeSpy.mock.calls[1][0]).toBe(100);
expect(onChangeSpy.mock.calls.length).toBe(1);
expect(onChangeSpy.mock.calls[0][0]).toBe(100);
});
});

View file

@ -5,9 +5,9 @@
*/
import React, { useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import { i18n } from '@kbn/i18n';
import { EuiRange } from '@elastic/eui';
import { useDebounceWithOptions } from '../helpers';
export const ValuesRangeInput = ({
value,
@ -20,7 +20,8 @@ export const ValuesRangeInput = ({
const MAX_NUMBER_OF_VALUES = 100;
const [inputValue, setInputValue] = useState(String(value));
useDebounce(
useDebounceWithOptions(
() => {
if (inputValue === '') {
return;
@ -28,6 +29,7 @@ export const ValuesRangeInput = ({
const inputNumber = Number(inputValue);
onChange(Math.min(MAX_NUMBER_OF_VALUES, Math.max(inputNumber, MIN_NUMBER_OF_VALUES)));
},
{ skipFirstRender: true },
256,
[inputValue]
);