mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Controls] Range slider a11y and performance improvements (#159271)
Closes https://github.com/elastic/kibana/issues/135466 ## Summary The main goal of this PR is to fix the serious "Buttons must have discernible text" a11y failure - this is accomplished by switching from building our own range slider button using `EuiFlexGroup` to instead using `EuiFormControlLayoutDelimited`, which both resolves these a11y issues and also fixes a rendering regression: | Before | After | |--------|-------| |  |  | As part of this, I also took some time to clean up some of the range slider code, which hasn't really been touched in awhile - this includes... - moving the debounce on range selections from the embeddable's `input$` subscription to the component itself, as described [here](https://github.com/elastic/kibana/pull/159271#discussion_r1226886857). - fixing a bug where resetting the range slider would unnecessarily cause unsaved changes, as described [here](https://github.com/elastic/kibana/pull/159271#discussion_r1226885018). - improving the `onClick` behaviour (with some notable limitations), as described [here](https://github.com/elastic/kibana/pull/159271#discussion_r1226934124). As a follow up, we need to move the "clear selections" button [to a hover action](https://github.com/elastic/kibana/issues/159395), which will enable us to then fully move forward with our transition to the `EuiDualRange` component. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] ~Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))~ > **Note** > Details provided [here](https://github.com/elastic/kibana/pull/159271#discussion_r1226934124) on why only partial keyboard support is currently supported - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
c915c0508b
commit
9b0f10629b
15 changed files with 307 additions and 310 deletions
|
@ -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<RangeSliderEmbeddableInput> =
|
||||
initialInput.explicitInput;
|
||||
const { value: valueB = ['', ''] }: Partial<RangeSliderEmbeddableInput> =
|
||||
newInput.explicitInput;
|
||||
return isEqual(valueA, valueB);
|
||||
},
|
||||
},
|
||||
[OPTIONS_LIST_CONTROL]: {
|
||||
getPanelIsEqual: (initialInput, newInput) => {
|
||||
if (!deepEqual(omit(initialInput, 'explicitInput'), omit(newInput, 'explicitInput'))) {
|
||||
|
|
|
@ -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<RangeSliderEmbeddableInput> & { type: string };
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
color: $euiTextSubduedColor;
|
||||
text-decoration: line-through;
|
||||
margin-left: $euiSizeS;
|
||||
font-weight: 300;
|
||||
font-weight: $euiFontWeightRegular;
|
||||
}
|
||||
|
||||
.optionsList__existsFilter {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<EuiFormControlLayoutDelimited
|
||||
fullWidth
|
||||
onClick={onClick}
|
||||
isLoading={isLoading}
|
||||
className="rangeSliderAnchor__button"
|
||||
data-test-subj={`range-slider-control-${id}`}
|
||||
startControl={
|
||||
<EuiFieldNumber
|
||||
controlOnly
|
||||
fullWidth
|
||||
value={value[0] === String(min) ? '' : value[0]}
|
||||
onChange={(event) => {
|
||||
onChange([event.target.value, value[1]]);
|
||||
}}
|
||||
placeholder={String(min)}
|
||||
isInvalid={isInvalid}
|
||||
className={'rangeSliderAnchor__fieldNumber'}
|
||||
data-test-subj={'rangeSlider__lowerBoundFieldNumber'}
|
||||
/>
|
||||
}
|
||||
endControl={
|
||||
<EuiFieldNumber
|
||||
controlOnly
|
||||
fullWidth
|
||||
value={value[1] === String(max) ? '' : value[1]}
|
||||
onChange={(event) => {
|
||||
onChange([value[0], event.target.value]);
|
||||
}}
|
||||
placeholder={String(max)}
|
||||
isInvalid={isInvalid}
|
||||
className={'rangeSliderAnchor__fieldNumber'}
|
||||
data-test-subj={'rangeSlider__upperBoundFieldNumber'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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<EuiDualRangeRef>(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 value = rangeSlider.select((state) => state.explicitInput.value);
|
||||
const [displayedValue, setDisplayedValue] = useState<RangeValue>(value ?? ['', '']);
|
||||
|
||||
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 debouncedOnChange = useMemo(
|
||||
() =>
|
||||
debounce((newRange: RangeValue) => {
|
||||
rangeSlider.dispatch.setSelectedRange(newRange);
|
||||
}, 750),
|
||||
[rangeSlider.dispatch]
|
||||
);
|
||||
|
||||
const hasAvailableRange = min !== '' && max !== '';
|
||||
useEffect(() => {
|
||||
debouncedOnChange(displayedValue);
|
||||
}, [debouncedOnChange, displayedValue]);
|
||||
|
||||
const hasLowerBoundSelection = value[0] !== '';
|
||||
const hasUpperBoundSelection = value[1] !== '';
|
||||
|
||||
const lowerBoundValue = parseFloat(value[0]);
|
||||
const upperBoundValue = 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;
|
||||
useEffect(() => {
|
||||
setDisplayedValue(value ?? ['', '']);
|
||||
}, [value]);
|
||||
|
||||
const button = (
|
||||
<button
|
||||
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
|
||||
className="rangeSliderAnchor__button"
|
||||
data-test-subj={`range-slider-control-${id}`}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldNumber
|
||||
controlOnly
|
||||
fullWidth
|
||||
className={`rangeSliderAnchor__fieldNumber ${
|
||||
hasLowerBoundSelection && isInvalid ? INVALID_CLASS : ''
|
||||
}`}
|
||||
value={hasLowerBoundSelection ? lowerBoundValue : ''}
|
||||
onChange={(event) => {
|
||||
rangeSlider.dispatch.setSelectedRange([
|
||||
event.target.value,
|
||||
isNaN(upperBoundValue) ? '' : String(upperBoundValue),
|
||||
]);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
placeholder={`${hasAvailableRange ? roundedMin : ''}`}
|
||||
isInvalid={isInvalid}
|
||||
data-test-subj="rangeSlider__lowerBoundFieldNumber"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText className="rangeSliderAnchor__delimiter" size="s" color="subdued">
|
||||
→
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldNumber
|
||||
controlOnly
|
||||
fullWidth
|
||||
className={`rangeSliderAnchor__fieldNumber ${
|
||||
hasUpperBoundSelection && isInvalid ? INVALID_CLASS : ''
|
||||
}`}
|
||||
value={hasUpperBoundSelection ? upperBoundValue : ''}
|
||||
onChange={(event) => {
|
||||
rangeSlider.dispatch.setSelectedRange([
|
||||
isNaN(lowerBoundValue) ? '' : String(lowerBoundValue),
|
||||
event.target.value,
|
||||
]);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
placeholder={`${hasAvailableRange ? roundedMax : ''}`}
|
||||
isInvalid={isInvalid}
|
||||
data-test-subj="rangeSlider__upperBoundFieldNumber"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{isLoading ? (
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
className="rangeSliderAnchor__spinner"
|
||||
data-test-subj="range-slider-loading-spinner"
|
||||
>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</button>
|
||||
<RangeSliderButton
|
||||
value={displayedValue}
|
||||
onChange={setDisplayedValue}
|
||||
isPopoverOpen={isPopoverOpen}
|
||||
setIsPopoverOpen={setIsPopoverOpen}
|
||||
/>
|
||||
);
|
||||
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<RangeSliderPopover rangeRef={rangeRef} />
|
||||
<RangeSliderPopover rangeRef={rangeRef} value={displayedValue} onChange={setDisplayedValue} />
|
||||
</EuiInputPopover>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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<typeof EuiDualRange>;
|
||||
|
||||
export const RangeSliderPopover: FC<{ rangeRef?: Ref<EuiDualRangeRef> }> = ({ rangeRef }) => {
|
||||
export const RangeSliderPopover: FC<{
|
||||
value: RangeValue;
|
||||
onChange: (newRange: RangeValue) => void;
|
||||
rangeRef?: Ref<EuiDualRangeRef>;
|
||||
}> = ({ onChange, value, rangeRef }) => {
|
||||
const [fieldFormatter, setFieldFormatter] = useState(() => (toFormat: string) => toFormat);
|
||||
|
||||
// Controls Services Context
|
||||
|
@ -39,79 +43,38 @@ export const RangeSliderPopover: FC<{ rangeRef?: Ref<EuiDualRangeRef> }> = ({ 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<number>(roundedMin);
|
||||
const [rangeSliderMax, setRangeSliderMax] = useState<number>(roundedMax);
|
||||
const [rangeSliderMin, setRangeSliderMin] = useState<number>(min);
|
||||
const [rangeSliderMax, setRangeSliderMax] = useState<number>(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<EuiDualRangeRef> }> = ({ 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 (
|
||||
<>
|
||||
<EuiPopoverTitle paddingSize="s">{title}</EuiPopoverTitle>
|
||||
|
@ -136,34 +110,31 @@ export const RangeSliderPopover: FC<{ rangeRef?: Ref<EuiDualRangeRef> }> = ({ ra
|
|||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiDualRange
|
||||
id={id}
|
||||
min={hasAvailableRange ? rangeSliderMin : 0}
|
||||
max={hasAvailableRange ? rangeSliderMax : 100}
|
||||
onChange={([newLowerBound, newUpperBound]) => {
|
||||
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"
|
||||
/>
|
||||
<EuiText
|
||||
size="s"
|
||||
color={errorMessage ? 'danger' : 'default'}
|
||||
data-test-subj="rangeSlider__helpText"
|
||||
>
|
||||
{errorMessage || helpText}
|
||||
</EuiText>
|
||||
{min !== -Infinity && max !== Infinity ? (
|
||||
<EuiDualRange
|
||||
id={id}
|
||||
min={rangeSliderMin}
|
||||
max={rangeSliderMax}
|
||||
onChange={([minSelection, maxSelection]) => {
|
||||
onChange([String(minSelection), String(maxSelection)]);
|
||||
}}
|
||||
value={value}
|
||||
ticks={ticks}
|
||||
levels={levels}
|
||||
showTicks
|
||||
fullWidth
|
||||
ref={rangeRef}
|
||||
data-test-subj="rangeSlider__slider"
|
||||
/>
|
||||
) : isInvalid ? (
|
||||
<EuiText size="s" data-test-subj="rangeSlider__helpText">
|
||||
{RangeSliderStrings.popover.getNoDataHelpText()}
|
||||
</EuiText>
|
||||
) : (
|
||||
<EuiText size="s" data-test-subj="rangeSlider__helpText">
|
||||
{RangeSliderStrings.popover.getNoAvailableDataHelpText()}
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={RangeSliderStrings.popover.getClearRangeButtonTitle()}>
|
||||
|
|
|
@ -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<RangeSliderEmbeddable | null>(null);
|
||||
export const useRangeSlider = (): RangeSliderEmbeddable => {
|
||||
|
@ -93,6 +97,7 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
// Internal data fetching state for this input control.
|
||||
private dataView?: DataView;
|
||||
private field?: DataViewField;
|
||||
private filters: Filter[] = [];
|
||||
|
||||
// state management
|
||||
public select: RangeSliderReduxEmbeddableTools['select'];
|
||||
|
@ -126,7 +131,6 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
this.dispatch = reduxEmbeddableTools.dispatch;
|
||||
this.onStateChange = reduxEmbeddableTools.onStateChange;
|
||||
this.cleanupStateTools = reduxEmbeddableTools.cleanup;
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
|
@ -136,19 +140,19 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
this.setInitializationFinished();
|
||||
}
|
||||
|
||||
this.runRangeSliderQuery()
|
||||
.catch((e) => {
|
||||
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<RangeSliderEmbeddableInput
|
|||
|
||||
// fetch available min/max when input changes
|
||||
this.subscriptions.add(
|
||||
dataFetchPipe.subscribe(async () =>
|
||||
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<RangeSliderEmbeddableInput
|
|||
try {
|
||||
this.field = this.dataView.getFieldByName(fieldName);
|
||||
if (this.field === undefined) {
|
||||
throw new Error(
|
||||
i18n.translate('controls.rangeSlider.errors.fieldNotFound', {
|
||||
defaultMessage: 'Could not locate field: {fieldName}',
|
||||
values: { fieldName },
|
||||
})
|
||||
);
|
||||
throw fieldMissingError(fieldName);
|
||||
}
|
||||
|
||||
this.dispatch.setField(this.field?.toSpec());
|
||||
|
@ -240,15 +241,7 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
const { dataView, field } = await this.getCurrentDataViewAndField();
|
||||
if (!dataView || !field) return;
|
||||
|
||||
const embeddableInput = this.getInput();
|
||||
const {
|
||||
ignoreParentSettings,
|
||||
fieldName,
|
||||
query,
|
||||
timeRange: globalTimeRange,
|
||||
timeslice,
|
||||
} = embeddableInput;
|
||||
let { filters = [] } = embeddableInput;
|
||||
const { fieldName } = this.getInput();
|
||||
|
||||
if (!field) {
|
||||
batch(() => {
|
||||
|
@ -258,9 +251,9 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
throw fieldMissingError(fieldName);
|
||||
}
|
||||
|
||||
if (ignoreParentSettings?.ignoreFilters) {
|
||||
filters = [];
|
||||
}
|
||||
const embeddableInput = this.getInput();
|
||||
const { ignoreParentSettings, timeRange: globalTimeRange, timeslice } = embeddableInput;
|
||||
let { filters = [] } = embeddableInput;
|
||||
|
||||
const timeRange =
|
||||
timeslice !== undefined
|
||||
|
@ -277,41 +270,36 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
}
|
||||
}
|
||||
|
||||
this.filters = filters;
|
||||
const { min, max } = await this.fetchMinMax({
|
||||
dataView,
|
||||
field,
|
||||
filters,
|
||||
query,
|
||||
}).catch((e) => {
|
||||
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<RangeSliderEmbeddableInput
|
|||
const resp = await lastValueFrom(searchSource.fetch$()).catch((e) => {
|
||||
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<RangeSliderEmbeddableInput
|
|||
private buildFilter = async () => {
|
||||
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<RangeSliderEmbeddableInput
|
|||
const params = {} as RangeFilterParams;
|
||||
|
||||
if (selectedMin) {
|
||||
params.gte = Math.max(parseFloat(selectedMin), parseFloat(availableMin));
|
||||
params.gte = Math.max(parseFloat(selectedMin), availableMin);
|
||||
}
|
||||
|
||||
if (selectedMax) {
|
||||
params.lte = Math.min(parseFloat(selectedMax), parseFloat(availableMax));
|
||||
params.lte = Math.min(parseFloat(selectedMax), availableMax);
|
||||
}
|
||||
|
||||
const rangeFilter = buildRangeFilter(field, params, dataView);
|
||||
|
||||
rangeFilter.meta.key = field?.name;
|
||||
rangeFilter.meta.type = 'range';
|
||||
rangeFilter.meta.params = params;
|
||||
|
@ -399,19 +384,11 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
if (!ignoreParentSettings?.ignoreValidations) {
|
||||
const searchSource = await this.dataService.searchSource.create();
|
||||
|
||||
filters.push(rangeFilter);
|
||||
|
||||
const timeFilter = this.dataService.timefilter.createFilter(dataView, timeRange);
|
||||
|
||||
if (timeFilter) {
|
||||
filters.push(timeFilter);
|
||||
}
|
||||
const filters = [...this.filters, rangeFilter];
|
||||
|
||||
searchSource.setField('size', 0);
|
||||
searchSource.setField('index', dataView);
|
||||
|
||||
searchSource.setField('filter', filters);
|
||||
|
||||
if (query) {
|
||||
searchSource.setField('query', query);
|
||||
}
|
||||
|
@ -423,7 +400,6 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
} = await lastValueFrom(searchSource.fetch$());
|
||||
|
||||
const docCount = typeof total === 'number' ? total : total?.value;
|
||||
|
||||
if (!docCount) {
|
||||
batch(() => {
|
||||
this.dispatch.setLoading(false);
|
||||
|
@ -445,10 +421,13 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
});
|
||||
};
|
||||
|
||||
public reload = () => {
|
||||
this.runRangeSliderQuery().catch((e) => {
|
||||
public reload = async () => {
|
||||
try {
|
||||
await this.runRangeSliderQuery();
|
||||
await this.buildFilter();
|
||||
} catch (e) {
|
||||
this.dispatch.setErrorMessage(e.message);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public destroy = () => {
|
||||
|
|
|
@ -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<RangeSliderReduxState>,
|
||||
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<RangeSliderReduxState>,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue