From 87d15ba2e1b9f7fef30eee2f202da78fe9635e07 Mon Sep 17 00:00:00 2001 From: Paulina Shakirova Date: Thu, 19 Dec 2024 17:33:11 +0100 Subject: [PATCH] [Controls] Debounce time slider selections (#201885) ## Summary This PR fixes the [[Controls] Debounce time slider selections](https://github.com/elastic/kibana/issues/193227) issue. Previously when the user was dragging the time slider, the dashboard would be updating constantly causing lagging on more complex dashboards. This PR adds a local state that will hold the updating values while the user is dragging, and updates the dashboards once the user stops dragging with a delay of 300. https://github.com/user-attachments/assets/45bca92e-f92a-4c1f-8417-a0a0818c7415 --------- Co-authored-by: Hannah Mudge Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../time_slider_popover_content.tsx | 85 ++++++++++++++----- .../get_timeslider_control_factory.tsx | 6 +- 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/plugins/controls/public/controls/timeslider_control/components/time_slider_popover_content.tsx b/src/plugins/controls/public/controls/timeslider_control/components/time_slider_popover_content.tsx index fc4d050d71d59..5bf94109d3b9f 100644 --- a/src/plugins/controls/public/controls/timeslider_control/components/time_slider_popover_content.tsx +++ b/src/plugins/controls/public/controls/timeslider_control/components/time_slider_popover_content.tsx @@ -8,6 +8,8 @@ */ import React from 'react'; +import { useMemo, useEffect, useState } from 'react'; +import { debounce } from 'lodash'; import { EuiButtonIcon, EuiRangeTick, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { TimeSliderStrings } from './time_slider_strings'; @@ -27,29 +29,63 @@ interface Props { compressed: boolean; } -export function TimeSliderPopoverContent(props: Props) { - const rangeInput = props.isAnchored ? ( +export function TimeSliderPopoverContent({ + isAnchored, + setIsAnchored, + value, + onChange, + stepSize, + ticks, + timeRangeMin, + timeRangeMax, + compressed, +}: Props) { + const [displayedValue, setDisplayedValue] = useState(value); + + const debouncedOnChange = useMemo( + () => + debounce((updateTimeslice: Timeslice | undefined) => { + onChange(updateTimeslice); + }, 750), + [onChange] + ); + + /** + * The following `useEffect` ensures that the changes to the value that come from the embeddable (for example, + * from the `clear` button on the dashboard) are reflected in the displayed value + */ + useEffect(() => { + setDisplayedValue(value); + }, [value]); + + const rangeInput = isAnchored ? ( { + setDisplayedValue(newValue as Timeslice); + debouncedOnChange(newValue); + }} + stepSize={stepSize} + ticks={ticks} + timeRangeMin={timeRangeMin} + timeRangeMax={timeRangeMax} + compressed={compressed} /> ) : ( { + setDisplayedValue(newValue as Timeslice); + debouncedOnChange(newValue); + }} + stepSize={stepSize} + ticks={ticks} + timeRangeMin={timeRangeMin} + timeRangeMax={timeRangeMax} + compressed={compressed} /> ); - const anchorStartToggleButtonLabel = props.isAnchored + const anchorStartToggleButtonLabel = isAnchored ? TimeSliderStrings.control.getUnpinStart() : TimeSliderStrings.control.getPinStart(); @@ -59,17 +95,24 @@ export function TimeSliderPopoverContent(props: Props) { gutterSize="none" data-test-subj="timeSlider-popoverContents" responsive={false} + onMouseUp={() => { + // when the pin is dropped (on mouse up), cancel any pending debounced changes and force the change + // in value to happen instantly (which, in turn, will re-calculate the from/to for the slider due to + // the `useEffect` above. + debouncedOnChange.cancel(); + onChange(displayedValue); + }} > { - const nextIsAnchored = !props.isAnchored; + const nextIsAnchored = !isAnchored; if (nextIsAnchored) { - props.onChange([props.timeRangeMin, props.value[1]]); + onChange([timeRangeMin, value[1]]); } - props.setIsAnchored(nextIsAnchored); + setIsAnchored(nextIsAnchored); }} aria-label={anchorStartToggleButtonLabel} data-test-subj="timeSlider__anchorStartToggleButton" diff --git a/src/plugins/controls/public/controls/timeslider_control/get_timeslider_control_factory.tsx b/src/plugins/controls/public/controls/timeslider_control/get_timeslider_control_factory.tsx index 59ad0a2a5076c..7e81fa075334e 100644 --- a/src/plugins/controls/public/controls/timeslider_control/get_timeslider_control_factory.tsx +++ b/src/plugins/controls/public/controls/timeslider_control/get_timeslider_control_factory.tsx @@ -270,7 +270,6 @@ export const getTimesliderControlFactory = (): ControlFactory< Component: (controlPanelClassNames) => { const [isAnchored, isPopoverOpen, timeRangeMeta, timeslice] = useBatchedPublishingSubjects(isAnchored$, isPopoverOpen$, timeRangeMeta$, timeslice$); - useEffect(() => { return () => { cleanupTimeRangeSubscription(); @@ -284,6 +283,9 @@ export const getTimesliderControlFactory = (): ControlFactory< const to = useMemo(() => { return timeslice ? timeslice[TO_INDEX] : timeRangeMeta.timeRangeMax; }, [timeslice, timeRangeMeta.timeRangeMax]); + const value: Timeslice = useMemo(() => { + return [from, to]; + }, [from, to]); return (