diff --git a/src/plugins/controls/common/control_group/control_group_panel_diff_system.ts b/src/plugins/controls/common/control_group/control_group_panel_diff_system.ts index a31fd0b514b4d..d373cf5796564 100644 --- a/src/plugins/controls/common/control_group/control_group_panel_diff_system.ts +++ b/src/plugins/controls/common/control_group/control_group_panel_diff_system.ts @@ -10,6 +10,7 @@ import deepEqual from 'fast-deep-equal'; import { omit, isEqual } from 'lodash'; import { OPTIONS_LIST_DEFAULT_SORT } from '../options_list/suggestions_sorting'; import { OptionsListEmbeddableInput, OPTIONS_LIST_CONTROL } from '../options_list/types'; +import { RangeSliderEmbeddableInput, RANGE_SLIDER_CONTROL } from '../range_slider/types'; import { ControlPanelState } from './types'; @@ -26,6 +27,19 @@ export const genericControlPanelDiffSystem: DiffSystem = { export const ControlPanelDiffSystems: { [key: string]: DiffSystem; } = { + [RANGE_SLIDER_CONTROL]: { + getPanelIsEqual: (initialInput, newInput) => { + if (!deepEqual(omit(initialInput, 'explicitInput'), omit(newInput, 'explicitInput'))) { + return false; + } + + const { value: valueA = ['', ''] }: Partial = + initialInput.explicitInput; + const { value: valueB = ['', ''] }: Partial = + newInput.explicitInput; + return isEqual(valueA, valueB); + }, + }, [OPTIONS_LIST_CONTROL]: { getPanelIsEqual: (initialInput, newInput) => { if (!deepEqual(omit(initialInput, 'explicitInput'), omit(newInput, 'explicitInput'))) { diff --git a/src/plugins/controls/common/range_slider/types.ts b/src/plugins/controls/common/range_slider/types.ts index 51c6d9e07e241..fdfc65d1d7c36 100644 --- a/src/plugins/controls/common/range_slider/types.ts +++ b/src/plugins/controls/common/range_slider/types.ts @@ -13,7 +13,7 @@ export const RANGE_SLIDER_CONTROL = 'rangeSliderControl'; export type RangeValue = [string, string]; export interface RangeSliderEmbeddableInput extends DataControlInput { - value: RangeValue; + value?: RangeValue; } export type RangeSliderInputWithType = Partial & { type: string }; diff --git a/src/plugins/controls/public/options_list/components/options_list.scss b/src/plugins/controls/public/options_list/components/options_list.scss index ad042916fff6e..0309437b8c9b3 100644 --- a/src/plugins/controls/public/options_list/components/options_list.scss +++ b/src/plugins/controls/public/options_list/components/options_list.scss @@ -33,7 +33,7 @@ color: $euiTextSubduedColor; text-decoration: line-through; margin-left: $euiSizeS; - font-weight: 300; + font-weight: $euiFontWeightRegular; } .optionsList__existsFilter { diff --git a/src/plugins/controls/public/range_slider/components/range_slider.scss b/src/plugins/controls/public/range_slider/components/range_slider.scss index d1a360b465962..abdd460da7286 100644 --- a/src/plugins/controls/public/range_slider/components/range_slider.scss +++ b/src/plugins/controls/public/range_slider/components/range_slider.scss @@ -17,42 +17,36 @@ } .rangeSliderAnchor__button { - display: flex; - align-items: center; width: 100%; height: 100%; - justify-content: space-between; background-color: $euiFormBackgroundColor; - @include euiFormControlSideBorderRadius($euiFormControlBorderRadius, $side: 'right', $internal: true); + padding: 0; + + .euiFormControlLayout__childrenWrapper { + border-radius: 0 $euiFormControlBorderRadius $euiFormControlBorderRadius 0 !important; + } .euiToolTipAnchor { width: 100%; } - .rangeSliderAnchor__delimiter { - background-color: unset; - padding: $euiSizeS*1.5 0; - } .rangeSliderAnchor__fieldNumber { font-weight: $euiFontWeightBold; box-shadow: none; text-align: center; background-color: unset; + &:invalid { + color: $euiTextSubduedColor; + text-decoration: line-through; + font-weight: $euiFontWeightRegular; + background-image: none; // hide the red bottom border + } + &::placeholder { font-weight: $euiFontWeightRegular; color: $euiColorMediumShade; text-decoration: none; } } - - .rangeSliderAnchor__fieldNumber--invalid { - text-decoration: line-through; - font-weight: $euiFontWeightRegular; - color: $euiColorMediumShade; - } - - .rangeSliderAnchor__spinner { - padding-right: $euiSizeS; - } } \ No newline at end of file diff --git a/src/plugins/controls/public/range_slider/components/range_slider_button.tsx b/src/plugins/controls/public/range_slider/components/range_slider_button.tsx new file mode 100644 index 0000000000000..d24f27e25979b --- /dev/null +++ b/src/plugins/controls/public/range_slider/components/range_slider_button.tsx @@ -0,0 +1,86 @@ +/* + * 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, { useCallback } from 'react'; + +import { EuiFieldNumber, EuiFormControlLayoutDelimited } from '@elastic/eui'; + +import './range_slider.scss'; +import { RangeValue } from '../../../common/range_slider/types'; +import { useRangeSlider } from '../embeddable/range_slider_embeddable'; + +export const RangeSliderButton = ({ + value, + onChange, + isPopoverOpen, + setIsPopoverOpen, +}: { + value: RangeValue; + isPopoverOpen: boolean; + setIsPopoverOpen: (open: boolean) => void; + onChange: (newRange: RangeValue) => void; +}) => { + const rangeSlider = useRangeSlider(); + + const min = rangeSlider.select((state) => state.componentState.min); + const max = rangeSlider.select((state) => state.componentState.max); + const isInvalid = rangeSlider.select((state) => state.componentState.isInvalid); + + const id = rangeSlider.select((state) => state.explicitInput.id); + + const isLoading = rangeSlider.select((state) => state.output.loading); + + const onClick = useCallback( + (event) => { + // the popover should remain open if the click/focus target is one of the number inputs + if (isPopoverOpen && event.target instanceof HTMLInputElement) { + return; + } + setIsPopoverOpen(true); + }, + [isPopoverOpen, setIsPopoverOpen] + ); + + return ( + { + onChange([event.target.value, value[1]]); + }} + placeholder={String(min)} + isInvalid={isInvalid} + className={'rangeSliderAnchor__fieldNumber'} + data-test-subj={'rangeSlider__lowerBoundFieldNumber'} + /> + } + endControl={ + { + onChange([value[0], event.target.value]); + }} + placeholder={String(max)} + isInvalid={isInvalid} + className={'rangeSliderAnchor__fieldNumber'} + data-test-subj={'rangeSlider__upperBoundFieldNumber'} + /> + } + /> + ); +}; diff --git a/src/plugins/controls/public/range_slider/components/range_slider_control.tsx b/src/plugins/controls/public/range_slider/components/range_slider_control.tsx index e483cf4bb16ae..8a9503ea48a5a 100644 --- a/src/plugins/controls/public/range_slider/components/range_slider_control.tsx +++ b/src/plugins/controls/public/range_slider/components/range_slider_control.tsx @@ -6,24 +6,18 @@ * Side Public License, v 1. */ -import React, { FC, useState, useRef } from 'react'; +import { debounce } from 'lodash'; +import React, { FC, useState, useRef, useMemo, useEffect } from 'react'; -import { - EuiFieldNumber, - EuiText, - EuiInputPopover, - EuiLoadingSpinner, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiInputPopover } from '@elastic/eui'; import { useRangeSlider } from '../embeddable/range_slider_embeddable'; import { RangeSliderPopover, EuiDualRangeRef } from './range_slider_popover'; -import './range_slider.scss'; import { ControlError } from '../../control_group/component/control_error_component'; - -const INVALID_CLASS = 'rangeSliderAnchor__fieldNumber--invalid'; +import { RangeValue } from '../../../common/range_slider/types'; +import { RangeSliderButton } from './range_slider_button'; +import './range_slider.scss'; export const RangeSliderControl: FC = () => { const rangeRef = useRef(null); @@ -31,92 +25,33 @@ export const RangeSliderControl: FC = () => { const rangeSlider = useRangeSlider(); - const min = rangeSlider.select((state) => state.componentState.min); - const max = rangeSlider.select((state) => state.componentState.max); const error = rangeSlider.select((state) => state.componentState.error); - const isInvalid = rangeSlider.select((state) => state.componentState.isInvalid); - - const id = rangeSlider.select((state) => state.explicitInput.id); - const value = rangeSlider.select((state) => state.explicitInput.value) ?? ['', '']; - const isLoading = rangeSlider.select((state) => state.output.loading); + const value = rangeSlider.select((state) => state.explicitInput.value); + const [displayedValue, setDisplayedValue] = useState(value ?? ['', '']); - const hasAvailableRange = min !== '' && max !== ''; - - const hasLowerBoundSelection = value[0] !== ''; - const hasUpperBoundSelection = value[1] !== ''; + const debouncedOnChange = useMemo( + () => + debounce((newRange: RangeValue) => { + rangeSlider.dispatch.setSelectedRange(newRange); + }, 750), + [rangeSlider.dispatch] + ); - const lowerBoundValue = parseFloat(value[0]); - const upperBoundValue = parseFloat(value[1]); - const minValue = parseFloat(min); - const maxValue = parseFloat(max); + useEffect(() => { + debouncedOnChange(displayedValue); + }, [debouncedOnChange, displayedValue]); - // EuiDualRange can only handle integers as min/max - const roundedMin = hasAvailableRange ? Math.floor(minValue) : minValue; - const roundedMax = hasAvailableRange ? Math.ceil(maxValue) : maxValue; + useEffect(() => { + setDisplayedValue(value ?? ['', '']); + }, [value]); const button = ( - + ); return error ? ( @@ -130,7 +65,9 @@ export const RangeSliderControl: FC = () => { className="rangeSlider__popoverOverride" anchorClassName="rangeSlider__anchorOverride" panelClassName="rangeSlider__panelOverride" - closePopover={() => setIsPopoverOpen(false)} + closePopover={() => { + setIsPopoverOpen(false); + }} anchorPosition="downCenter" attachToAnchor={false} disableFocusTrap @@ -138,7 +75,7 @@ export const RangeSliderControl: FC = () => { rangeRef.current?.onResize(width); }} > - + ); }; diff --git a/src/plugins/controls/public/range_slider/components/range_slider_popover.tsx b/src/plugins/controls/public/range_slider/components/range_slider_popover.tsx index c3b2ccbe676f4..65d61a467f309 100644 --- a/src/plugins/controls/public/range_slider/components/range_slider_popover.tsx +++ b/src/plugins/controls/public/range_slider/components/range_slider_popover.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC, ComponentProps, Ref, useEffect, useState } from 'react'; +import React, { FC, ComponentProps, Ref, useEffect, useState, useMemo } from 'react'; import useMount from 'react-use/lib/useMount'; import { @@ -14,9 +14,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiDualRange, - EuiText, EuiToolTip, EuiButtonIcon, + EuiText, } from '@elastic/eui'; import type { EuiDualRangeClass } from '@elastic/eui/src/components/form/range/dual_range'; @@ -28,7 +28,11 @@ import { useRangeSlider } from '../embeddable/range_slider_embeddable'; // Unfortunately, wrapping EuiDualRange in `withEuiTheme` has created this annoying/verbose typing export type EuiDualRangeRef = EuiDualRangeClass & ComponentProps; -export const RangeSliderPopover: FC<{ rangeRef?: Ref }> = ({ rangeRef }) => { +export const RangeSliderPopover: FC<{ + value: RangeValue; + onChange: (newRange: RangeValue) => void; + rangeRef?: Ref; +}> = ({ onChange, value, rangeRef }) => { const [fieldFormatter, setFieldFormatter] = useState(() => (toFormat: string) => toFormat); // Controls Services Context @@ -39,79 +43,38 @@ export const RangeSliderPopover: FC<{ rangeRef?: Ref }> = ({ ra // Select current state from Redux using multiple selectors to avoid rerenders. const dataViewId = rangeSlider.select((state) => state.output.dataViewId); - const fieldSpec = rangeSlider.select((state) => state.componentState.field); + const id = rangeSlider.select((state) => state.explicitInput.id); - const isInvalid = rangeSlider.select((state) => state.componentState.isInvalid); - const max = rangeSlider.select((state) => state.componentState.max); - const min = rangeSlider.select((state) => state.componentState.min); const title = rangeSlider.select((state) => state.explicitInput.title); - const value = rangeSlider.select((state) => state.explicitInput.value) ?? ['', '']; - - const hasAvailableRange = min !== '' && max !== ''; - const hasLowerBoundSelection = value[0] !== ''; - const hasUpperBoundSelection = value[1] !== ''; - const lowerBoundSelection = parseFloat(value[0]); - const upperBoundSelection = parseFloat(value[1]); - const minValue = parseFloat(min); - const maxValue = parseFloat(max); - - // EuiDualRange can only handle integers as min/max - const roundedMin = hasAvailableRange ? Math.floor(minValue) : minValue; - const roundedMax = hasAvailableRange ? Math.ceil(maxValue) : maxValue; + const min = rangeSlider.select((state) => state.componentState.min); + const max = rangeSlider.select((state) => state.componentState.max); + const fieldSpec = rangeSlider.select((state) => state.componentState.field); + const isInvalid = rangeSlider.select((state) => state.componentState.isInvalid); // Caches min and max displayed on popover open so the range slider doesn't resize as selections change - const [rangeSliderMin, setRangeSliderMin] = useState(roundedMin); - const [rangeSliderMax, setRangeSliderMax] = useState(roundedMax); + const [rangeSliderMin, setRangeSliderMin] = useState(min); + const [rangeSliderMax, setRangeSliderMax] = useState(max); useMount(() => { + const [lowerBoundSelection, upperBoundSelection] = [parseFloat(value[0]), parseFloat(value[1])]; + setRangeSliderMin( Math.min( - roundedMin, + min, isNaN(lowerBoundSelection) ? Infinity : lowerBoundSelection, isNaN(upperBoundSelection) ? Infinity : upperBoundSelection ) ); setRangeSliderMax( Math.max( - roundedMax, + max, isNaN(lowerBoundSelection) ? -Infinity : lowerBoundSelection, isNaN(upperBoundSelection) ? -Infinity : upperBoundSelection ) ); }); - const errorMessage = ''; - let helpText = ''; - - if (!hasAvailableRange) { - helpText = RangeSliderStrings.popover.getNoAvailableDataHelpText(); - } else if (isInvalid) { - helpText = RangeSliderStrings.popover.getNoDataHelpText(); - } - - const displayedValue = [ - hasLowerBoundSelection - ? String(lowerBoundSelection) - : hasAvailableRange - ? String(roundedMin) - : '', - hasUpperBoundSelection - ? String(upperBoundSelection) - : hasAvailableRange - ? String(roundedMax) - : '', - ] as RangeValue; - - const ticks = []; - const levels = []; - - if (hasAvailableRange) { - ticks.push({ value: rangeSliderMin, label: fieldFormatter(String(rangeSliderMin)) }); - ticks.push({ value: rangeSliderMax, label: fieldFormatter(String(rangeSliderMax)) }); - levels.push({ min: roundedMin, max: roundedMax, color: 'success' }); - } - // derive field formatter from fieldSpec and dataViewId useEffect(() => { (async () => { @@ -126,6 +89,17 @@ export const RangeSliderPopover: FC<{ rangeRef?: Ref }> = ({ ra })(); }, [fieldSpec, dataViewId, getDataViewById]); + const ticks = useMemo(() => { + return [ + { value: min, label: fieldFormatter(String(min)) }, + { value: max, label: fieldFormatter(String(max)) }, + ]; + }, [min, max, fieldFormatter]); + + const levels = useMemo(() => { + return [{ min, max, color: 'success' }]; + }, [min, max]); + return ( <> {title} @@ -136,34 +110,31 @@ export const RangeSliderPopover: FC<{ rangeRef?: Ref }> = ({ ra responsive={false} > - { - const updatedLowerBound = - typeof newLowerBound === 'number' ? String(newLowerBound) : value[0]; - const updatedUpperBound = - typeof newUpperBound === 'number' ? String(newUpperBound) : value[1]; - - rangeSlider.dispatch.setSelectedRange([updatedLowerBound, updatedUpperBound]); - }} - value={displayedValue} - ticks={hasAvailableRange ? ticks : undefined} - levels={hasAvailableRange ? levels : undefined} - showTicks={hasAvailableRange} - disabled={!hasAvailableRange} - fullWidth - ref={rangeRef} - data-test-subj="rangeSlider__slider" - /> - - {errorMessage || helpText} - + {min !== -Infinity && max !== Infinity ? ( + { + onChange([String(minSelection), String(maxSelection)]); + }} + value={value} + ticks={ticks} + levels={levels} + showTicks + fullWidth + ref={rangeRef} + data-test-subj="rangeSlider__slider" + /> + ) : isInvalid ? ( + + {RangeSliderStrings.popover.getNoDataHelpText()} + + ) : ( + + {RangeSliderStrings.popover.getNoAvailableDataHelpText()} + + )} diff --git a/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.tsx b/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.tsx index e1bacc7327310..ad0c2aae0eaf3 100644 --- a/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.tsx +++ b/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.tsx @@ -13,7 +13,7 @@ import { batch } from 'react-redux'; import { get, isEqual } from 'lodash'; import deepEqual from 'fast-deep-equal'; import { Subscription, lastValueFrom } from 'rxjs'; -import { debounceTime, distinctUntilChanged, skip, map } from 'rxjs/operators'; +import { distinctUntilChanged, skip, map } from 'rxjs/operators'; import { compareFilters, @@ -21,7 +21,6 @@ import { COMPARE_ALL_OPTIONS, RangeFilterParams, Filter, - Query, } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; @@ -63,7 +62,12 @@ interface RangeSliderDataFetchProps { } const fieldMissingError = (fieldName: string) => - new Error(`field ${fieldName} not found in index pattern`); + new Error( + i18n.translate('controls.rangeSlider.errors.fieldNotFound', { + defaultMessage: 'Could not locate field: {fieldName}', + values: { fieldName }, + }) + ); export const RangeSliderControlContext = createContext(null); export const useRangeSlider = (): RangeSliderEmbeddable => { @@ -93,6 +97,7 @@ export class RangeSliderEmbeddable extends Embeddable { - batch(() => { - this.dispatch.setLoading(false); - this.dispatch.setErrorMessage(e.message); - }); - }) - .then(async () => { - if (initialValue) { - this.setInitializationFinished(); - } - this.setupSubscriptions(); + try { + await this.runRangeSliderQuery(); + await this.buildFilter(); + if (initialValue) { + this.setInitializationFinished(); + } + } catch (e) { + batch(() => { + this.dispatch.setLoading(false); + this.dispatch.setErrorMessage(e.message); }); + } + this.setupSubscriptions(); }; private setupSubscriptions = () => { @@ -169,19 +173,21 @@ export class RangeSliderEmbeddable extends Embeddable - this.runRangeSliderQuery().catch((e) => { + dataFetchPipe.subscribe(async (changes) => { + try { + await this.runRangeSliderQuery(); + await this.buildFilter(); + } catch (e) { this.dispatch.setErrorMessage(e.message); - }) - ) + } + }) ); - // build filters when value change + // build filters when value changes this.subscriptions.add( this.getInput$() .pipe( - debounceTime(400), - distinctUntilChanged((a, b) => isEqual(a.value, b.value)), + distinctUntilChanged((a, b) => isEqual(a.value ?? ['', ''], b.value ?? ['', ''])), skip(1) // skip the first input update because initial filters will be built by initialize. ) .subscribe(this.buildFilter) @@ -217,12 +223,7 @@ export class RangeSliderEmbeddable extends Embeddable { @@ -258,9 +251,9 @@ export class RangeSliderEmbeddable extends Embeddable { throw e; }); this.dispatch.setMinMax({ - min: `${min ?? ''}`, - max: `${max ?? ''}`, - }); - // build filter with new min/max - await this.buildFilter().catch((e) => { - throw e; + min: `${min ?? '-Infinity'}`, + max: `${max ?? 'Infinity'}`, }); }; private fetchMinMax = async ({ dataView, field, - filters, - query, }: { dataView: DataView; field: DataViewField; - filters: Filter[]; - query?: Query; - }) => { + }): Promise<{ min?: number; max?: number }> => { const searchSource = await this.dataService.searchSource.create(); searchSource.setField('size', 0); searchSource.setField('index', dataView); - searchSource.setField('filter', filters); + const { ignoreParentSettings, query } = this.getInput(); + + if (!ignoreParentSettings?.ignoreFilters) { + searchSource.setField('filter', this.filters); + } if (query) { searchSource.setField('query', query); @@ -343,8 +331,8 @@ export class RangeSliderEmbeddable extends Embeddable { throw e; }); - const min = get(resp, 'rawResponse.aggregations.minAgg.value', ''); - const max = get(resp, 'rawResponse.aggregations.maxAgg.value', ''); + const min = get(resp, 'rawResponse.aggregations.minAgg.value'); + const max = get(resp, 'rawResponse.aggregations.maxAgg.value'); return { min, max }; }; @@ -352,15 +340,13 @@ export class RangeSliderEmbeddable extends Embeddable { const { componentState: { min: availableMin, max: availableMax }, - explicitInput: { - query, - timeRange, - filters = [], - ignoreParentSettings, - value: [selectedMin, selectedMax] = ['', ''], - }, + explicitInput: { value }, } = this.getState(); - const hasData = !isEmpty(availableMin) && !isEmpty(availableMax); + + const { ignoreParentSettings, query } = this.getInput(); + + const [selectedMin, selectedMax] = value ?? ['', '']; + const hasData = availableMin !== undefined && availableMax !== undefined; const hasLowerSelection = !isEmpty(selectedMin); const hasUpperSelection = !isEmpty(selectedMax); const hasEitherSelection = hasLowerSelection || hasUpperSelection; @@ -382,15 +368,14 @@ export class RangeSliderEmbeddable extends Embeddable { this.dispatch.setLoading(false); @@ -445,10 +421,13 @@ export class RangeSliderEmbeddable extends Embeddable { - this.runRangeSliderQuery().catch((e) => { + public reload = async () => { + try { + await this.runRangeSliderQuery(); + await this.buildFilter(); + } catch (e) { this.dispatch.setErrorMessage(e.message); - }); + } }; public destroy = () => { diff --git a/src/plugins/controls/public/range_slider/range_slider_reducers.ts b/src/plugins/controls/public/range_slider/range_slider_reducers.ts index e8fcf0a10b2c3..c1b5c93c4ce25 100644 --- a/src/plugins/controls/public/range_slider/range_slider_reducers.ts +++ b/src/plugins/controls/public/range_slider/range_slider_reducers.ts @@ -16,9 +16,9 @@ import { RangeSliderReduxState } from './types'; import { RangeValue } from '../../common/range_slider/types'; export const getDefaultComponentState = (): RangeSliderReduxState['componentState'] => ({ - min: '', - max: '', isInvalid: false, + min: -Infinity, + max: Infinity, }); export const rangeSliderReducers = { @@ -53,8 +53,8 @@ export const rangeSliderReducers = { state: WritableDraft, action: PayloadAction<{ min: string; max: string }> ) => { - state.componentState.min = action.payload.min; - state.componentState.max = action.payload.max; + state.componentState.min = Math.floor(parseFloat(action.payload.min)); + state.componentState.max = Math.ceil(parseFloat(action.payload.max)); }, publishFilters: ( state: WritableDraft, diff --git a/src/plugins/controls/public/range_slider/types.ts b/src/plugins/controls/public/range_slider/types.ts index ad321271631e0..8b283d300e6ae 100644 --- a/src/plugins/controls/public/range_slider/types.ts +++ b/src/plugins/controls/public/range_slider/types.ts @@ -15,8 +15,8 @@ import { ControlOutput } from '../types'; // Component state is only used by public components. export interface RangeSliderComponentState { field?: FieldSpec; - min: string; - max: string; + min: number; + max: number; error?: string; isInvalid?: boolean; } diff --git a/test/functional/apps/dashboard_elements/controls/range_slider.ts b/test/functional/apps/dashboard_elements/controls/range_slider.ts index f1ccf742ad844..6d895ec7f9d2d 100644 --- a/test/functional/apps/dashboard_elements/controls/range_slider.ts +++ b/test/functional/apps/dashboard_elements/controls/range_slider.ts @@ -132,7 +132,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await saveButton.isEnabled()).to.be(false); await dashboardControls.controlsEditorSetfield('dayOfWeek', RANGE_SLIDER_CONTROL); await dashboardControls.controlEditorSave(); - await dashboardControls.rangeSliderWaitForLoading(); + await dashboardControls.rangeSliderWaitForLoading(firstId); await dashboardControls.validateRange('placeholder', firstId, '0', '6'); await dashboardControls.validateRange('value', firstId, '', ''); @@ -164,11 +164,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'value' ); expect(upperBoundSelection).to.be('2'); + await dashboardControls.rangeSliderWaitForLoading(firstId); }); it('applies filter from the first control on the second control', async () => { - await dashboardControls.rangeSliderWaitForLoading(); const secondId = (await dashboardControls.getAllControlIds())[1]; + await dashboardControls.rangeSliderWaitForLoading(secondId); await dashboardControls.validateRange('placeholder', secondId, '100', '1000'); await dashboard.clearUnsavedChanges(); }); @@ -183,15 +184,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('making changes to range causes unsaved changes', async () => { const firstId = (await dashboardControls.getAllControlIds())[0]; - await dashboardControls.rangeSliderSetLowerBound(firstId, '0'); + await dashboardControls.rangeSliderSetLowerBound(firstId, '2'); await dashboardControls.rangeSliderSetUpperBound(firstId, '3'); - await dashboardControls.rangeSliderWaitForLoading(); + await dashboardControls.rangeSliderWaitForLoading(firstId); await testSubjects.existOrFail('dashboardUnsavedChangesBadge'); }); it('changes to range can be discarded', async () => { const firstId = (await dashboardControls.getAllControlIds())[0]; - await dashboardControls.validateRange('value', firstId, '0', '3'); + await dashboardControls.validateRange('value', firstId, '2', '3'); await dashboard.clickCancelOutOfEditMode(); await dashboardControls.validateRange('value', firstId, '', ''); }); @@ -216,7 +217,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardControls.rangeSliderSetUpperBound(firstId, '400'); }); - it('disables range slider when no data available', async () => { + it('hides range slider in popover when no data available', async () => { await dashboardControls.createControl({ controlType: RANGE_SLIDER_CONTROL, dataViewTitle: 'logstash-*', @@ -226,9 +227,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const secondId = (await dashboardControls.getAllControlIds())[1]; await dashboardControls.rangeSliderOpenPopover(secondId); await dashboardControls.rangeSliderPopoverAssertOpen(); - expect( - await dashboardControls.rangeSliderGetDualRangeAttribute(secondId, 'disabled') - ).to.be('true'); + await testSubjects.missingOrFail('rangeSlider__slider'); expect((await testSubjects.getVisibleText('rangeSlider__helpText')).length).to.be.above(0); }); }); @@ -250,7 +249,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Applies dashboard query to range slider control', async () => { const firstId = (await dashboardControls.getAllControlIds())[0]; - await dashboardControls.rangeSliderWaitForLoading(); + await dashboardControls.rangeSliderWaitForLoading(firstId); await dashboardControls.validateRange('placeholder', firstId, '100', '300'); await queryBar.setQuery(''); await queryBar.submitQuery(); diff --git a/test/functional/apps/dashboard_elements/controls/replace_controls.ts b/test/functional/apps/dashboard_elements/controls/replace_controls.ts index 4cfbe1a7ff560..7b17c143c515a 100644 --- a/test/functional/apps/dashboard_elements/controls/replace_controls.ts +++ b/test/functional/apps/dashboard_elements/controls/replace_controls.ts @@ -41,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const replaceWithRangeSlider = async (controlId: string, field: string) => { await changeFieldType(controlId, field, RANGE_SLIDER_CONTROL); await retry.try(async () => { - await dashboardControls.rangeSliderWaitForLoading(); + await dashboardControls.rangeSliderWaitForLoading(controlId); await dashboardControls.verifyControlType(controlId, 'range-slider-control'); }); }; @@ -102,8 +102,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataViewTitle: 'animals-*', fieldName: 'weightLbs', }); - await dashboardControls.rangeSliderWaitForLoading(); controlId = (await dashboardControls.getAllControlIds())[0]; + await dashboardControls.rangeSliderWaitForLoading(controlId); }); afterEach(async () => { diff --git a/test/functional/apps/dashboard_elements/controls/time_slider.ts b/test/functional/apps/dashboard_elements/controls/time_slider.ts index ef75d4e74b24e..06d2a9b10024c 100644 --- a/test/functional/apps/dashboard_elements/controls/time_slider.ts +++ b/test/functional/apps/dashboard_elements/controls/time_slider.ts @@ -96,8 +96,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('applies filter from the first control on the second control', async () => { - await dashboardControls.rangeSliderWaitForLoading(); const secondId = (await dashboardControls.getAllControlIds())[1]; + await dashboardControls.rangeSliderWaitForLoading(secondId); await dashboardControls.validateRange('placeholder', secondId, '101', '1000'); }); diff --git a/test/functional/page_objects/dashboard_page_controls.ts b/test/functional/page_objects/dashboard_page_controls.ts index a82662c5326c5..0625ead5f0357 100644 --- a/test/functional/page_objects/dashboard_page_controls.ts +++ b/test/functional/page_objects/dashboard_page_controls.ts @@ -336,11 +336,28 @@ export class DashboardPageControls extends FtrService { } public async verifyControlType(controlId: string, expectedType: string) { - const controlButton = await this.find.byXPath( - `//div[@id='controlFrame--${controlId}']//button` - ); - const testSubj = await controlButton.getAttribute('data-test-subj'); - expect(testSubj).to.equal(`${expectedType}-${controlId}`); + let controlButton; + switch (expectedType) { + case OPTIONS_LIST_CONTROL: { + controlButton = await this.find.byXPath(`//div[@id='controlFrame--${controlId}']//button`); + break; + } + case RANGE_SLIDER_CONTROL: { + controlButton = await this.find.byXPath( + `//div[@id='controlFrame--${controlId}']//div[contains(@class, 'rangeSliderAnchor__button')]` + ); + break; + } + default: { + this.log.error('An invalid control type was provided.'); + break; + } + } + + if (controlButton) { + const testSubj = await controlButton.getAttribute('data-test-subj'); + expect(testSubj).to.equal(`${expectedType}-${controlId}`); + } } // Options list functions @@ -636,10 +653,7 @@ export class DashboardPageControls extends FtrService { attribute ); } - public async rangeSliderGetDualRangeAttribute(controlId: string, attribute: string) { - this.log.debug(`Getting range slider dual range ${attribute} for ${controlId}`); - return await this.testSubjects.getAttribute(`rangeSlider__slider`, attribute); - } + public async rangeSliderSetLowerBound(controlId: string, value: string) { this.log.debug(`Setting range slider lower bound to ${value}`); await this.testSubjects.setValue( @@ -665,7 +679,8 @@ export class DashboardPageControls extends FtrService { public async rangeSliderEnsurePopoverIsClosed(controlId: string) { this.log.debug(`Opening popover for Range Slider: ${controlId}`); - await this.testSubjects.click(`range-slider-control-${controlId}`); + const controlLabel = await this.find.byXPath(`//div[@data-control-id='${controlId}']//label`); + await controlLabel.click(); await this.testSubjects.waitForDeleted(`rangeSlider-control-actions`); } @@ -677,8 +692,10 @@ export class DashboardPageControls extends FtrService { }); } - public async rangeSliderWaitForLoading() { - await this.testSubjects.waitForDeleted('range-slider-loading-spinner'); + public async rangeSliderWaitForLoading(controlId: string) { + await this.find.waitForDeletedByCssSelector( + `[data-test-subj="range-slider-control-${controlId}"] .euiLoadingSpinner` + ); } public async rangeSliderClearSelection(controlId: string) { diff --git a/x-pack/test/functional/apps/dashboard/group3/drilldowns/dashboard_to_dashboard_drilldown.ts b/x-pack/test/functional/apps/dashboard/group3/drilldowns/dashboard_to_dashboard_drilldown.ts index 7cd8d5c8a455d..e7afd4f9761da 100644 --- a/x-pack/test/functional/apps/dashboard/group3/drilldowns/dashboard_to_dashboard_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/group3/drilldowns/dashboard_to_dashboard_drilldown.ts @@ -309,7 +309,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboardControls.optionsListOpenPopover(optionsListControl); await PageObjects.dashboardControls.optionsListPopoverSelectOption('CN'); await PageObjects.dashboardControls.optionsListPopoverSelectOption('US'); - await PageObjects.dashboardControls.rangeSliderWaitForLoading(); // wait for range slider to respond to options list selections before proceeding + await PageObjects.dashboardControls.rangeSliderWaitForLoading(rangeSliderControl); // wait for range slider to respond to options list selections before proceeding await PageObjects.dashboardControls.rangeSliderSetLowerBound(rangeSliderControl, '1000'); await PageObjects.dashboardControls.rangeSliderSetUpperBound(rangeSliderControl, '15000'); await PageObjects.dashboard.clickQuickSave();