[Controls] Removes timeslider in favor of new timeslider control (#138931)

* Removed timeslider in favor of new timeslider control

* Removed time slider import

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* Fixed i18n errors

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Catherine Liu 2022-08-23 12:49:35 -07:00 committed by GitHub
parent f2c20b7c9f
commit 239def566c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 7 additions and 1677 deletions

View file

@ -1,47 +0,0 @@
/*
* 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 {
EmbeddableStateWithType,
EmbeddablePersistableStateService,
} from '@kbn/embeddable-plugin/common';
import { SavedObjectReference } from '@kbn/core/types';
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common';
import { TimeSliderControlEmbeddableInput } from './types';
type TimeSliderInputWithType = Partial<TimeSliderControlEmbeddableInput> & { type: string };
const dataViewReferenceName = 'timeSliderDataView';
export const createTimeSliderInject = (): EmbeddablePersistableStateService['inject'] => {
return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => {
const workingState = { ...state } as EmbeddableStateWithType | TimeSliderInputWithType;
references.forEach((reference) => {
if (reference.name === dataViewReferenceName) {
(workingState as TimeSliderInputWithType).dataViewId = reference.id;
}
});
return workingState as EmbeddableStateWithType;
};
};
export const createTimeSliderExtract = (): EmbeddablePersistableStateService['extract'] => {
return (state: EmbeddableStateWithType) => {
const workingState = { ...state } as EmbeddableStateWithType | TimeSliderInputWithType;
const references: SavedObjectReference[] = [];
if ('dataViewId' in workingState) {
references.push({
name: dataViewReferenceName,
type: DATA_VIEW_SAVED_OBJECT_TYPE,
id: workingState.dataViewId!,
});
delete workingState.dataViewId;
}
return { state: workingState as EmbeddableStateWithType, references };
};
};

View file

@ -1,15 +0,0 @@
/*
* 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 { DataControlInput } from '../../types';
export const TIME_SLIDER_CONTROL = 'timeSlider';
export interface TimeSliderControlEmbeddableInput extends DataControlInput {
value?: [number | null, number | null];
}

View file

@ -36,5 +36,3 @@ export {
// Control Type exports
export { OPTIONS_LIST_CONTROL, type OptionsListEmbeddableInput } from './options_list/types';
export { type RangeSliderEmbeddableInput, RANGE_SLIDER_CONTROL } from './range_slider/types';
export { TIME_SLIDER_CONTROL } from './control_types/time_slider/types';

View file

@ -8,7 +8,6 @@
import { OptionsListEmbeddableFactory } from '../options_list';
import { RangeSliderEmbeddableFactory } from '../range_slider';
import { TimesliderEmbeddableFactory } from '../control_types/time_slider';
import { ControlsService } from '../services/controls';
import { ControlFactory } from '..';
@ -26,9 +25,4 @@ export const populateStorybookControlFactories = (controlsServiceStub: ControlsS
const rangeSliderControlFactory = rangeSliderFactoryStub as unknown as ControlFactory;
rangeSliderControlFactory.getDefaultInput = () => ({});
controlsServiceStub.registerControlType(rangeSliderControlFactory);
const timesliderFactoryStub = new TimesliderEmbeddableFactory();
const timeSliderControlFactory = timesliderFactoryStub as unknown as ControlFactory;
timeSliderControlFactory.getDefaultInput = () => ({});
controlsServiceStub.registerControlType(timeSliderControlFactory);
};

View file

@ -1,196 +0,0 @@
/*
* 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, { FC, useCallback, useState } from 'react';
import moment from 'moment';
import { EuiFormControlLayout } from '@elastic/eui';
import { TimeSliderProps, TimeSlider } from '../time_slider.component';
export default {
title: 'Time Slider',
description: '',
};
const TimeSliderWrapper: FC<Omit<TimeSliderProps, 'onChange' | 'fieldName' | 'id'>> = (props) => {
const [value, setValue] = useState(props.value);
const onChange = useCallback(
(newValue: [number | null, number | null]) => {
const lowValue = newValue[0];
const highValue = newValue[1];
setValue([lowValue, highValue]);
},
[setValue]
);
return (
<div style={{ width: '600px' }}>
<EuiFormControlLayout style={{ width: '100%' }}>
<TimeSlider
id="time_slider_control"
{...props}
value={value}
fieldName={'Field Name'}
onChange={onChange}
/>
</EuiFormControlLayout>
</div>
);
};
const undefinedValue: [null, null] = [null, null];
const undefinedRange: [undefined, undefined] = [undefined, undefined];
export const TimeSliderNoValuesOrRange = () => {
// If range is undefined, that should be inndicate that we are loading the range
return <TimeSliderWrapper value={undefinedValue} />;
};
export const TimeSliderUndefinedRangeNoValue = () => {
// If a range is [undefined, undefined] then it was loaded, but no values were found.
return <TimeSliderWrapper range={undefinedRange} value={undefinedValue} />;
};
export const TimeSliderUndefinedRangeWithValue = () => {
const lastWeek = moment().subtract(7, 'days');
const now = moment();
return (
<TimeSliderWrapper range={undefinedRange} value={[lastWeek.unix() * 1000, now.unix() * 1000]} />
);
};
export const TimeSliderWithRangeAndNoValue = () => {
const lastWeek = moment().subtract(7, 'days');
const now = moment();
return (
<TimeSliderWrapper range={[lastWeek.unix() * 1000, now.unix() * 1000]} value={undefinedValue} />
);
};
export const TimeSliderWithRangeAndLowerValue = () => {
const lastWeek = moment().subtract(7, 'days');
const now = moment();
const threeDays = moment().subtract(3, 'days');
return (
<TimeSliderWrapper
range={[lastWeek.unix() * 1000, now.unix() * 1000]}
value={[threeDays.unix() * 1000, null]}
/>
);
};
export const TimeSliderWithRangeAndUpperValue = () => {
const lastWeek = moment().subtract(7, 'days');
const now = moment();
const threeDays = moment().subtract(3, 'days');
return (
<TimeSliderWrapper
range={[lastWeek.unix() * 1000, now.unix() * 1000]}
value={[null, threeDays.unix() * 1000]}
/>
);
};
export const TimeSliderWithLowRangeOverlap = () => {
const lastWeek = moment().subtract(7, 'days');
const now = moment();
const threeDays = moment().subtract(3, 'days');
const twoDays = moment().subtract(2, 'days');
return (
<TimeSliderWrapper
range={[threeDays.unix() * 1000, now.unix() * 1000]}
value={[lastWeek.unix() * 1000, twoDays.unix() * 1000]}
/>
);
};
export const TimeSliderWithLowRangeOverlapAndIgnoredValidation = () => {
const lastWeek = moment().subtract(7, 'days');
const now = moment();
const threeDays = moment().subtract(3, 'days');
const twoDays = moment().subtract(2, 'days');
return (
<TimeSliderWrapper
ignoreValidation={true}
range={[threeDays.unix() * 1000, now.unix() * 1000]}
value={[lastWeek.unix() * 1000, twoDays.unix() * 1000]}
/>
);
};
export const TimeSliderWithRangeLowerThanValue = () => {
const twoWeeksAgo = moment().subtract(14, 'days');
const lastWeek = moment().subtract(7, 'days');
const now = moment();
const threeDays = moment().subtract(3, 'days');
return (
<TimeSliderWrapper
range={[twoWeeksAgo.unix() * 1000, lastWeek.unix() * 1000]}
value={[threeDays.unix() * 1000, now.unix() * 1000]}
/>
);
};
export const TimeSliderWithRangeHigherThanValue = () => {
const twoWeeksAgo = moment().subtract(14, 'days');
const lastWeek = moment().subtract(7, 'days');
const now = moment();
const threeDays = moment().subtract(3, 'days');
return (
<TimeSliderWrapper
value={[twoWeeksAgo.unix() * 1000, lastWeek.unix() * 1000]}
range={[threeDays.unix() * 1000, now.unix() * 1000]}
/>
);
};
export const PartialValueLowerThanRange = () => {
// Selected value is March 8 -> March 9
// Range is March 11 -> 25
const eightDaysAgo = moment().subtract(8, 'days');
const lastWeek = moment().subtract(7, 'days');
const today = moment();
return (
<TimeSliderWrapper
value={[null, eightDaysAgo.unix() * 1000]}
range={[lastWeek.unix() * 1000, today.unix() * 1000]}
/>
);
};
export const PartialValueHigherThanRange = () => {
// Selected value is March 8 -> March 9
// Range is March 11 -> 25
const eightDaysAgo = moment().subtract(8, 'days');
const lastWeek = moment().subtract(7, 'days');
const today = moment();
return (
<TimeSliderWrapper
range={[eightDaysAgo.unix() * 1000, lastWeek.unix() * 1000]}
value={[today.unix() * 1000, null]}
/>
);
};

View file

@ -1,11 +0,0 @@
/*
* 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.
*/
export { TimesliderEmbeddableFactory } from './time_slider_embeddable_factory';
export { type TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types';
export {} from '../../../common';

View file

@ -1,47 +0,0 @@
.timeSlider__anchorOverride {
display:block;
>div {
height: 100%;
}
}
.timeSlider__popoverOverride {
width: 100%;
max-width: 100%;
height: 100%;
}
.timeSlider__panelOverride {
min-width: $euiSizeXXL * 15;
}
.timeSlider__anchor {
text-decoration: none;
width: 100%;
background-color: $euiFormBackgroundColor;
box-shadow: none;
@include euiFormControlSideBorderRadius($euiFormControlBorderRadius, $side: 'right', $internal: true);
overflow: hidden;
height: 100%;
&:enabled:focus {
background-color: $euiFormBackgroundColor;
}
.euiText {
background-color: $euiFormBackgroundColor;
}
.timeSlider__anchorText {
font-weight: $euiFontWeightBold;
}
.timeSlider__anchorText--default {
color: $euiColorMediumShade;
}
.timeSlider__anchorText--invalid {
text-decoration: line-through;
color: $euiColorMediumShade;
}
}

View file

@ -1,343 +0,0 @@
/*
* 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, { FC, useState, useMemo, useCallback } from 'react';
import { isNil } from 'lodash';
import {
EuiText,
EuiLoadingSpinner,
EuiInputPopover,
EuiPopoverTitle,
EuiSpacer,
EuiFlexItem,
EuiFlexGroup,
EuiToolTip,
EuiButtonIcon,
} from '@elastic/eui';
import { EuiRangeTick } from '@elastic/eui/src/components/form/range/range_ticks';
import moment from 'moment-timezone';
import { calcAutoIntervalNear } from '@kbn/data-plugin/common';
import { ValidatedDualRange } from '@kbn/kibana-react-plugin/public';
import { TimeSliderStrings } from './time_slider_strings';
import './time_slider.component.scss';
function getScaledDateFormat(interval: number): string {
if (interval >= moment.duration(1, 'y').asMilliseconds()) {
return 'YYYY';
}
if (interval >= moment.duration(1, 'd').asMilliseconds()) {
return 'MMM D';
}
if (interval >= moment.duration(6, 'h').asMilliseconds()) {
return 'Do HH';
}
if (interval >= moment.duration(1, 'h').asMilliseconds()) {
return 'HH:mm';
}
if (interval >= moment.duration(1, 'm').asMilliseconds()) {
return 'HH:mm';
}
if (interval >= moment.duration(1, 's').asMilliseconds()) {
return 'mm:ss';
}
return 'ss.SSS';
}
export function getInterval(min: number, max: number, steps = 6): number {
const duration = max - min;
let interval = calcAutoIntervalNear(steps, duration).asMilliseconds();
// Sometimes auto interval is not quite right and returns 2X or 3X requested ticks
// Adjust the interval to get closer to the requested number of ticks
const actualSteps = duration / interval;
if (actualSteps > steps * 1.5) {
const factor = Math.round(actualSteps / steps);
interval *= factor;
} else if (actualSteps < 5) {
interval *= 0.5;
}
return interval;
}
export interface TimeSliderProps {
id: string;
range?: [number | undefined, number | undefined];
value: [number | null, number | null];
onChange: (range: [number | null, number | null]) => void;
dateFormat?: string;
timezone?: string;
fieldName: string;
ignoreValidation?: boolean;
}
const isValidRange = (maybeRange: TimeSliderProps['range']): maybeRange is [number, number] => {
return maybeRange !== undefined && !isNil(maybeRange[0]) && !isNil(maybeRange[1]);
};
const unselectedClass = 'timeSlider__anchorText--default';
const validClass = 'timeSlider__anchorText';
const invalidClass = 'timeSlider__anchorText--invalid';
export const TimeSlider: FC<TimeSliderProps> = (props) => {
const defaultProps = {
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
ignoreValidation: false,
timezone: 'Browser',
...props,
};
const { range, value, timezone, dateFormat, fieldName, ignoreValidation } = defaultProps;
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const togglePopover = useCallback(() => {
setIsPopoverOpen(!isPopoverOpen);
}, [isPopoverOpen, setIsPopoverOpen]);
const getTimezone = useCallback(() => {
const detectedTimezone = moment.tz.guess();
return timezone === 'Browser' ? detectedTimezone : timezone;
}, [timezone]);
const epochToKbnDateFormat = useCallback(
(epoch: number) => {
const tz = getTimezone();
return moment.tz(epoch, tz).format(dateFormat);
},
[dateFormat, getTimezone]
);
// If we don't have a range or we have is loading, show the loading state
const hasRange = range !== undefined;
// We have values if we have a range or value entry for both position
const hasValues =
(value[0] !== null || (hasRange && range[0] !== undefined)) &&
(value[1] !== null || (hasRange && range[1] !== undefined));
let valueText: JSX.Element | null = null;
if (hasValues) {
let lower = value[0] !== null ? value[0] : range![0]!;
let upper = value[1] !== null ? value[1] : range![1]!;
if (value[0] !== null && lower > upper) {
upper = lower;
} else if (value[1] !== null && lower > upper) {
lower = upper;
}
const hasLowerValueInRange =
value[0] !== null && isValidRange(range) && value[0] >= range[0] && value[0] <= range[1];
// It's out of range if the upper value is above the upper range or below the lower range
const hasUpperValueInRange =
value[1] !== null && isValidRange(range) && value[1] <= range[1] && value[1] >= range[0];
let lowClass = unselectedClass;
let highClass = unselectedClass;
if (value[0] !== null && (hasLowerValueInRange || ignoreValidation)) {
lowClass = validClass;
} else if (value[0] !== null) {
lowClass = invalidClass;
}
if (value[1] !== null && (hasUpperValueInRange || ignoreValidation)) {
highClass = validClass;
} else if (value[1] !== null) {
highClass = invalidClass;
}
// if no value then anchorText default
// if hasLowerValueInRange || skipValidation then anchor text
// else strikethrough
valueText = (
<EuiText className="eui-textTruncate" size="s">
<span className={lowClass}>{epochToKbnDateFormat(lower)}</span>
&nbsp;&nbsp;&nbsp;&nbsp;
<span className={highClass}>{epochToKbnDateFormat(upper)}</span>
</EuiText>
);
}
const button = (
<button
className="timeSlider__anchor eui-textTruncate"
color="text"
onClick={togglePopover}
data-test-subj={`timeSlider-${props.id}`}
>
{valueText}
{!hasRange ? (
<div data-test-subj="timeSlider-loading-spinner" className="timeSliderAnchor__spinner">
<EuiLoadingSpinner />
</div>
) : undefined}
</button>
);
return (
<EuiInputPopover
input={button}
isOpen={isPopoverOpen}
className="timeSlider__popoverOverride"
anchorClassName="timeSlider__anchorOverride"
panelClassName="timeSlider__panelOverride"
closePopover={() => setIsPopoverOpen(false)}
panelPaddingSize="s"
anchorPosition="downCenter"
disableFocusTrap
attachToAnchor={false}
>
{isValidRange(range) ? (
<TimeSliderComponentPopover
id={props.id}
range={range}
value={value}
onChange={props.onChange}
getTimezone={getTimezone}
epochToKbnDateFormat={epochToKbnDateFormat}
fieldName={fieldName}
/>
) : (
<TimeSliderComponentPopoverNoDocuments />
)}
</EuiInputPopover>
);
};
const TimeSliderComponentPopoverNoDocuments: FC = () => {
return <EuiText size="s">{TimeSliderStrings.noDocumentsPopover.getLabel()}</EuiText>;
};
export const TimeSliderComponentPopover: FC<
TimeSliderProps & {
range: [number, number];
getTimezone: () => string;
epochToKbnDateFormat: (epoch: number) => string;
}
> = ({ range, value, onChange, getTimezone, epochToKbnDateFormat, fieldName }) => {
const [lowerBound, upperBound] = range;
let [lowerValue, upperValue] = value;
if (lowerValue === null) {
lowerValue = lowerBound;
}
if (upperValue === null) {
upperValue = upperBound;
}
const fullRange = useMemo(
() => [Math.min(lowerValue!, lowerBound), Math.max(upperValue!, upperBound)],
[lowerValue, lowerBound, upperValue, upperBound]
);
const getTicks = useCallback(
(min: number, max: number, interval: number): EuiRangeTick[] => {
const format = getScaledDateFormat(interval);
const tz = getTimezone();
let tick = Math.ceil(min / interval) * interval;
const ticks: EuiRangeTick[] = [];
while (tick < max) {
ticks.push({
value: tick,
label: moment.tz(tick, tz).format(format),
});
tick += interval;
}
return ticks;
},
[getTimezone]
);
const ticks = useMemo(() => {
const interval = getInterval(fullRange[0], fullRange[1]);
return getTicks(fullRange[0], fullRange[1], interval);
}, [fullRange, getTicks]);
const onChangeHandler = useCallback(
([_min, _max]: [number | string, number | string]) => {
// If a value is undefined and the number that is given here matches the range bounds
// then we will ignore it, becuase they probably didn't actually select that value
const report: [number | null, number | null] = [null, null];
let min: number;
let max: number;
if (typeof _min === 'string') {
min = parseFloat(_min);
min = isNaN(min) ? range[0] : min;
} else {
min = _min;
}
if (typeof _max === 'string') {
max = parseFloat(_max);
max = isNaN(max) ? range[0] : max;
} else {
max = _max;
}
if (value[0] !== null || min !== range[0]) {
report[0] = min;
}
if (value[1] !== null || max !== range[1]) {
report[1] = max;
}
onChange(report);
},
[onChange, value, range]
);
const levels = [{ min: range[0], max: range[1], color: 'success' }];
return (
<>
<EuiPopoverTitle paddingSize="s">{fieldName}</EuiPopoverTitle>
<EuiText textAlign="center" size="s">
{epochToKbnDateFormat(lowerValue)} - {epochToKbnDateFormat(upperValue)}
</EuiText>
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<ValidatedDualRange
id={`range${fieldName}`}
max={fullRange[1]}
min={fullRange[0]}
onChange={onChangeHandler}
step={undefined}
value={[lowerValue, upperValue]}
fullWidth
ticks={ticks}
levels={levels}
showTicks
disabled={false}
allowEmptyRange
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip content={TimeSliderStrings.resetButton.getLabel()}>
<EuiButtonIcon
iconType="eraser"
color="danger"
onClick={() => onChange([null, null])}
aria-label={TimeSliderStrings.resetButton.getLabel()}
data-test-subj="timeSlider__clearRangeButton"
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</>
);
};

View file

@ -1,79 +0,0 @@
/*
* 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, { FC, useCallback, useMemo } from 'react';
import { BehaviorSubject } from 'rxjs';
import { debounce } from 'lodash';
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
import { timeSliderReducers } from './time_slider_reducers';
import { TimeSlider as Component } from './time_slider.component';
import { TimeSliderReduxState, TimeSliderSubjectState } from './types';
interface TimeSliderProps {
componentStateSubject: BehaviorSubject<TimeSliderSubjectState>;
dateFormat: string;
timezone: string;
fieldName: string;
ignoreValidation: boolean;
}
export const TimeSlider: FC<TimeSliderProps> = ({
componentStateSubject,
dateFormat,
timezone,
fieldName,
ignoreValidation,
}) => {
const {
useEmbeddableDispatch,
useEmbeddableSelector: select,
actions: { selectRange },
} = useReduxEmbeddableContext<TimeSliderReduxState, typeof timeSliderReducers>();
const dispatch = useEmbeddableDispatch();
const availableRange = select((state) => state.componentState.range);
const value = select((state) => state.explicitInput.value);
const id = select((state) => state.explicitInput.id);
const { min, max } = availableRange
? availableRange
: ({} as {
min?: number;
max?: number;
});
const dispatchChange = useCallback(
(range: [number | null, number | null]) => {
dispatch(selectRange(range));
},
[dispatch, selectRange]
);
const debouncedDispatchChange = useMemo(() => debounce(dispatchChange, 500), [dispatchChange]);
const onChangeComplete = useCallback(
(range: [number | null, number | null]) => {
debouncedDispatchChange(range);
},
[debouncedDispatchChange]
);
return (
<Component
id={id}
onChange={onChangeComplete}
value={value ?? [null, null]}
range={[min, max]}
dateFormat={dateFormat}
timezone={timezone}
fieldName={fieldName}
ignoreValidation={ignoreValidation}
/>
);
};

View file

@ -1,300 +0,0 @@
/*
* 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 { of } from 'rxjs';
import { delay, map } from 'rxjs/operators';
import { TimeSliderControlEmbeddableInput } from '.';
import { TimeSliderControlEmbeddable } from './time_slider_embeddable';
import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { pluginServices } from '../../services';
import { TestScheduler } from 'rxjs/testing';
import { buildRangeFilter } from '@kbn/es-query';
import { ReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
const buildFilter = (range: [number | null, number | null]) => {
const filterPieces: Record<string, number> = {};
if (range[0] !== null) {
filterPieces.gte = range[0];
}
if (range[1] !== null) {
filterPieces.lte = range[1];
}
const filter = buildRangeFilter(
stubLogstashDataView.getFieldByName('bytes')!,
filterPieces,
stubLogstashDataView
);
filter.meta.key = 'bytes';
return filter;
};
const rangeMin = 20;
const rangeMax = 30;
const range = { min: rangeMin, max: rangeMax };
const lowerValue: [number, number] = [15, 25];
const upperValue: [number, number] = [25, 35];
const partialLowValue: [number, null] = [25, null];
const partialHighValue: [null, number] = [null, 25];
const withinRangeValue: [number, number] = [21, 29];
const outOfRangeValue: [number, number] = [31, 40];
const rangeFilter = buildFilter([rangeMin, rangeMax]);
const lowerValueFilter = buildFilter(lowerValue);
const lowerValuePartialFilter = buildFilter([20, 25]);
const upperValueFilter = buildFilter(upperValue);
const upperValuePartialFilter = buildFilter([25, 30]);
const partialLowValueFilter = buildFilter(partialLowValue);
const partialHighValueFilter = buildFilter(partialHighValue);
const withinRangeValueFilter = buildFilter(withinRangeValue);
const outOfRangeValueFilter = buildFilter(outOfRangeValue);
const baseInput: TimeSliderControlEmbeddableInput = {
id: 'id',
fieldName: 'bytes',
dataViewId: stubLogstashDataView.id!,
};
const mockReduxEmbeddablePackage = {
createTools: () => {},
} as unknown as ReduxEmbeddablePackage;
describe('Time Slider Control Embeddable', () => {
const services = pluginServices.getServices();
const fetchRange = jest.spyOn(services.data, 'fetchFieldRange');
const getDataView = jest.spyOn(services.data, 'getDataView');
const fetchRange$ = jest.spyOn(services.data, 'fetchFieldRange$');
const getDataView$ = jest.spyOn(services.data, 'getDataView$');
beforeEach(() => {
jest.resetAllMocks();
fetchRange.mockResolvedValue(range);
fetchRange$.mockReturnValue(of(range).pipe(delay(100)));
getDataView.mockResolvedValue(stubLogstashDataView);
getDataView$.mockReturnValue(of(stubLogstashDataView));
});
describe('outputting filters', () => {
let testScheduler: TestScheduler;
beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
const testFilterOutput = (
input: any,
expectedFilterAfterRangeFetch: any,
mockRange: { min?: number; max?: number } = range
) => {
testScheduler.run(({ expectObservable, cold }) => {
fetchRange$.mockReturnValue(cold('--b', { b: mockRange }));
const expectedMarbles = 'a-b';
const expectedValues = {
a: undefined,
b: expectedFilterAfterRangeFetch ? [expectedFilterAfterRangeFetch] : undefined,
};
const embeddable = new TimeSliderControlEmbeddable(mockReduxEmbeddablePackage, input, {});
const source$ = embeddable.getOutput$().pipe(map((o) => o.filters));
expectObservable(source$).toBe(expectedMarbles, expectedValues);
});
};
it('outputs no filter when no value is given', () => {
testFilterOutput(baseInput, undefined);
});
it('outputs the value filter after the range is fetched', () => {
testFilterOutput({ ...baseInput, value: withinRangeValue }, withinRangeValueFilter);
});
it('outputs a partial filter for a low partial value', () => {
testFilterOutput({ ...baseInput, value: partialLowValue }, partialLowValueFilter);
});
it('outputs a partial filter for a high partial value', () => {
testFilterOutput({ ...baseInput, value: partialHighValue }, partialHighValueFilter);
});
describe('with validation', () => {
it('outputs a partial value filter if value is below range', () => {
testFilterOutput({ ...baseInput, value: lowerValue }, lowerValuePartialFilter);
});
it('outputs a partial value filter if value is above range', () => {
testFilterOutput({ ...baseInput, value: upperValue }, upperValuePartialFilter);
});
it('outputs range filter value if value is completely out of range', () => {
testFilterOutput({ ...baseInput, value: outOfRangeValue }, rangeFilter);
});
it('outputs no filter when no range available', () => {
testFilterOutput({ ...baseInput, value: withinRangeValue }, undefined, {});
});
});
describe('with validation off', () => {
it('outputs the lower value filter', () => {
testFilterOutput(
{ ...baseInput, ignoreParentSettings: { ignoreValidations: true }, value: lowerValue },
lowerValueFilter
);
});
it('outputs the uppwer value filter', () => {
testFilterOutput(
{ ...baseInput, ignoreParentSettings: { ignoreValidations: true }, value: upperValue },
upperValueFilter
);
});
it('outputs the out of range filter', () => {
testFilterOutput(
{
...baseInput,
ignoreParentSettings: { ignoreValidations: true },
value: outOfRangeValue,
},
outOfRangeValueFilter
);
});
it('outputs the value filter when no range found', () => {
testFilterOutput(
{
...baseInput,
ignoreParentSettings: { ignoreValidations: true },
value: withinRangeValue,
},
withinRangeValueFilter,
{ min: undefined, max: undefined }
);
});
});
});
describe('fetching range', () => {
it('fetches range on init', () => {
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
testScheduler.run(({ cold, expectObservable }) => {
const mockRange = { min: 1, max: 2 };
fetchRange$.mockReturnValue(cold('--b', { b: mockRange }));
const expectedMarbles = 'a-b';
const expectedValues = {
a: undefined,
b: mockRange,
};
const embeddable = new TimeSliderControlEmbeddable(
mockReduxEmbeddablePackage,
baseInput,
{}
);
const source$ = embeddable.getComponentState$().pipe(map((state) => state.range));
const { fieldName, ...inputForFetch } = baseInput;
expectObservable(source$).toBe(expectedMarbles, expectedValues);
expect(fetchRange$).toBeCalledWith(stubLogstashDataView, fieldName, {
...inputForFetch,
filters: undefined,
query: undefined,
timeRange: undefined,
viewMode: 'edit',
});
});
});
it('fetches range on input change', () => {
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
testScheduler.run(({ cold, expectObservable, flush }) => {
const mockRange = { min: 1, max: 2 };
fetchRange$.mockReturnValue(cold('a', { a: mockRange }));
const embeddable = new TimeSliderControlEmbeddable(
mockReduxEmbeddablePackage,
baseInput,
{}
);
const updatedInput = { ...baseInput, fieldName: '@timestamp' };
embeddable.updateInput(updatedInput);
expect(fetchRange$).toBeCalledTimes(2);
expect(fetchRange$.mock.calls[1][1]).toBe(updatedInput.fieldName);
});
});
it('passes input to fetch range to build the query', () => {
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
testScheduler.run(({ cold, expectObservable, flush }) => {
const mockRange = { min: 1, max: 2 };
fetchRange$.mockReturnValue(cold('a', { a: mockRange }));
const input = {
...baseInput,
query: {} as any,
filters: {} as any,
timeRange: {} as any,
};
new TimeSliderControlEmbeddable(mockReduxEmbeddablePackage, input, {});
expect(fetchRange$).toBeCalledTimes(1);
const args = fetchRange$.mock.calls[0][2];
expect(args.query).toBe(input.query);
expect(args.filters).toBe(input.filters);
expect(args.timeRange).toBe(input.timeRange);
});
});
it('does not pass ignored parent settings', () => {
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
testScheduler.run(({ cold, expectObservable, flush }) => {
const mockRange = { min: 1, max: 2 };
fetchRange$.mockReturnValue(cold('a', { a: mockRange }));
const input = {
...baseInput,
query: '' as any,
filters: {} as any,
timeRange: {} as any,
ignoreParentSettings: { ignoreFilters: true, ignoreQuery: true, ignoreTimerange: true },
};
new TimeSliderControlEmbeddable(mockReduxEmbeddablePackage, input, {});
expect(fetchRange$).toBeCalledTimes(1);
const args = fetchRange$.mock.calls[0][2];
expect(args.query).not.toBe(input.query);
expect(args.filters).not.toBe(input.filters);
expect(args.timeRange).not.toBe(input.timeRange);
});
});
});
});

View file

@ -1,334 +0,0 @@
/*
* 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 { compareFilters, buildRangeFilter, RangeFilterParams } from '@kbn/es-query';
import React from 'react';
import ReactDOM from 'react-dom';
import { isEqual } from 'lodash';
import deepEqual from 'fast-deep-equal';
import { merge, Subscription, BehaviorSubject, Observable } from 'rxjs';
import { map, distinctUntilChanged, skip, take, mergeMap } from 'rxjs/operators';
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
import { ReduxEmbeddableTools, ReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
import { DataView } from '@kbn/data-views-plugin/public';
import { TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types';
import { TIME_SLIDER_CONTROL } from '../..';
import { ControlsSettingsService } from '../../services/settings';
import { ControlsDataService } from '../../services/data';
import { ControlOutput } from '../..';
import { pluginServices } from '../../services';
import { TimeSlider as TimeSliderComponent } from './time_slider';
import { timeSliderReducers } from './time_slider_reducers';
import { TimeSliderReduxState, TimeSliderSubjectState } from './types';
const diffDataFetchProps = (current?: any, last?: any) => {
if (!current || !last) return false;
const { filters: currentFilters, ...currentWithoutFilters } = current;
const { filters: lastFilters, ...lastWithoutFilters } = last;
if (!deepEqual(currentWithoutFilters, lastWithoutFilters)) return false;
if (!compareFilters(lastFilters ?? [], currentFilters ?? [])) return false;
return true;
};
export class TimeSliderControlEmbeddable extends Embeddable<
TimeSliderControlEmbeddableInput,
ControlOutput
> {
public readonly type = TIME_SLIDER_CONTROL;
public deferEmbeddableLoad = true;
private subscriptions: Subscription = new Subscription();
private node?: HTMLElement;
// Internal data fetching state for this input control.
private dataView?: DataView;
private componentState: TimeSliderSubjectState;
private componentStateSubject$ = new BehaviorSubject<TimeSliderSubjectState>({
range: undefined,
loading: false,
});
// Internal state subject will let us batch updates to the externally accessible state subject
private internalComponentStateSubject$ = new BehaviorSubject<TimeSliderSubjectState>({
range: undefined,
loading: false,
});
private internalOutput: ControlOutput;
private fetchRange$: ControlsDataService['fetchFieldRange$'];
private getDataView$: ControlsDataService['getDataView$'];
private getDateFormat: ControlsSettingsService['getDateFormat'];
private getTimezone: ControlsSettingsService['getTimezone'];
private reduxEmbeddableTools: ReduxEmbeddableTools<
TimeSliderReduxState,
typeof timeSliderReducers
>;
constructor(
reduxEmbeddablePackage: ReduxEmbeddablePackage,
input: TimeSliderControlEmbeddableInput,
output: ControlOutput,
parent?: IContainer
) {
super(input, output, parent); // get filters for initial output...
const {
data: { fetchFieldRange$, getDataView$ },
settings: { getDateFormat, getTimezone },
} = pluginServices.getServices();
this.fetchRange$ = fetchFieldRange$;
this.getDataView$ = getDataView$;
this.getDateFormat = getDateFormat;
this.getTimezone = getTimezone;
this.componentState = { loading: true };
this.updateComponentState(this.componentState, true);
this.internalOutput = {};
// build redux embeddable tools
this.reduxEmbeddableTools = reduxEmbeddablePackage.createTools<
TimeSliderReduxState,
typeof timeSliderReducers
>({
embeddable: this,
reducers: timeSliderReducers,
});
this.initialize();
}
private initialize() {
// If value is undefined, then we can be finished with initialization because we're not going to output a filter
if (this.getInput().value === undefined) {
this.setInitializationFinished();
}
this.setupSubscriptions();
}
private setupSubscriptions() {
// We need to fetch data when any of these values change
const dataFetchPipe = this.getInput$().pipe(
map((newInput) => ({
lastReloadRequestTime: newInput.lastReloadRequestTime,
dataViewId: newInput.dataViewId,
fieldName: newInput.fieldName,
timeRange: newInput.timeRange,
filters: newInput.filters,
query: newInput.query,
})),
distinctUntilChanged(diffDataFetchProps)
);
// When data fetch pipe emits, we start the fetch
this.subscriptions.add(dataFetchPipe.subscribe(this.fetchAvailableTimerange));
const availableRangePipe = this.internalComponentStateSubject$.pipe(
map((state) => (state.range ? { min: state.range.min, max: state.range.max } : {})),
distinctUntilChanged((a, b) => isEqual(a, b))
);
this.subscriptions.add(
merge(
this.getInput$().pipe(
skip(1), // Skip the first input value
distinctUntilChanged((a, b) => isEqual(a.value, b.value))
),
availableRangePipe.pipe(skip(1))
).subscribe(() => {
this.setInitializationFinished();
this.buildFilter();
this.componentStateSubject$.next(this.componentState);
})
);
}
private buildFilter = () => {
const { fieldName, value, ignoreParentSettings } = this.getInput();
const min = value ? value[0] : null;
const max = value ? value[1] : null;
const hasRange =
this.componentState.range?.max !== undefined && this.componentState.range?.min !== undefined;
this.getCurrentDataView$().subscribe((dataView) => {
const range: RangeFilterParams = {};
let filterMin: number | undefined;
let filterMax: number | undefined;
const field = dataView.getFieldByName(fieldName);
if (ignoreParentSettings?.ignoreValidations) {
if (min !== null) {
range.gte = min;
}
if (max !== null) {
range.lte = max;
}
} else {
// If we have a value or a range use the min/max of those, otherwise undefined
if (min !== null && this.componentState.range!.min !== undefined) {
filterMin = Math.max(min || 0, this.componentState.range!.min || 0);
}
if (max !== null && this.componentState.range!.max) {
filterMax = Math.min(
max || Number.MAX_SAFE_INTEGER,
this.componentState.range!.max || Number.MAX_SAFE_INTEGER
);
}
// Last check, if the value is completely outside the range then we will just default to the range
if (
hasRange &&
((min !== null && min > this.componentState.range!.max!) ||
(max !== null && max < this.componentState.range!.min!))
) {
filterMin = this.componentState.range!.min;
filterMax = this.componentState.range!.max;
}
if (hasRange && filterMin !== undefined) {
range.gte = filterMin;
}
if (hasRange && filterMax !== undefined) {
range.lte = filterMax;
}
}
if (range.lte !== undefined || range.gte !== undefined) {
const rangeFilter = buildRangeFilter(field!, range, dataView);
rangeFilter.meta.key = field?.name;
this.updateInternalOutput({ filters: [rangeFilter] }, true);
this.updateComponentState({ loading: false });
} else {
this.updateInternalOutput({ filters: undefined, dataViewId: dataView.id }, true);
this.updateComponentState({ loading: false });
}
});
};
private updateComponentState(changes: Partial<TimeSliderSubjectState>, publish = false) {
this.componentState = {
...this.componentState,
...changes,
};
this.internalComponentStateSubject$.next(this.componentState);
if (publish) {
this.componentStateSubject$.next(this.componentState);
}
}
private updateInternalOutput(changes: Partial<ControlOutput>, publish = false) {
this.internalOutput = {
...this.internalOutput,
...changes,
};
if (publish) {
this.updateOutput(this.internalOutput);
}
}
private getCurrentDataView$ = () => {
const { dataViewId } = this.getInput();
if (this.dataView && this.dataView.id === dataViewId)
return new Observable<DataView>((subscriber) => {
subscriber.next(this.dataView);
subscriber.complete();
});
return this.getDataView$(dataViewId);
};
private fetchAvailableTimerange = () => {
this.updateComponentState({ loading: true }, true);
this.updateInternalOutput({ loading: true }, true);
const { fieldName, ignoreParentSettings, query, filters, timeRange, ...input } =
this.getInput();
const inputForFetch = {
...input,
...(ignoreParentSettings?.ignoreQuery ? {} : { query }),
...(ignoreParentSettings?.ignoreFilters ? {} : { filters }),
...(ignoreParentSettings?.ignoreTimerange ? {} : { timeRange }),
};
try {
this.getCurrentDataView$()
.pipe(
mergeMap((dataView) => this.fetchRange$(dataView, fieldName, inputForFetch)),
take(1)
)
.subscribe(({ min, max }) => {
this.updateInternalOutput({ loading: false });
this.updateComponentState({
range: {
min: min === null ? undefined : min,
max: max === null ? undefined : max,
},
loading: false,
});
});
} catch (e) {
this.updateComponentState({ loading: false }, true);
this.updateInternalOutput({ loading: false }, true);
}
};
public getComponentState$ = () => {
return this.componentStateSubject$;
};
public destroy = () => {
super.destroy();
this.subscriptions.unsubscribe();
};
public reload = () => {
this.fetchAvailableTimerange();
};
public render = (node: HTMLElement) => {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
const { Wrapper: TimeSliderControlReduxWrapper } = this.reduxEmbeddableTools;
ReactDOM.render(
<TimeSliderControlReduxWrapper>
<TimeSliderComponent
componentStateSubject={this.componentStateSubject$}
timezone={this.getTimezone()}
dateFormat={this.getDateFormat()}
fieldName={this.getInput().fieldName}
ignoreValidation={
this.getInput().ignoreParentSettings !== undefined &&
this.getInput().ignoreParentSettings?.ignoreValidations !== undefined &&
this.getInput().ignoreParentSettings?.ignoreValidations!
}
/>
</TimeSliderControlReduxWrapper>,
node
);
};
}

View file

@ -1,69 +0,0 @@
/*
* 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 deepEqual from 'fast-deep-equal';
import { lazyLoadReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
import { TIME_SLIDER_CONTROL } from '../..';
import { ControlEmbeddable, DataControlField, IEditableControlFactory } from '../../types';
import {
createOptionsListExtract,
createOptionsListInject,
} from '../../../common/options_list/options_list_persistable_state';
import { TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types';
import { TimeSliderStrings } from './time_slider_strings';
export class TimesliderEmbeddableFactory
implements EmbeddableFactoryDefinition, IEditableControlFactory<TimeSliderControlEmbeddableInput>
{
public type = TIME_SLIDER_CONTROL;
public canCreateNew = () => false;
constructor() {}
public async create(initialInput: TimeSliderControlEmbeddableInput, parent?: IContainer) {
const reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
const { TimeSliderControlEmbeddable } = await import('./time_slider_embeddable');
return Promise.resolve(
new TimeSliderControlEmbeddable(reduxEmbeddablePackage, initialInput, {}, parent)
);
}
public presaveTransformFunction = (
newInput: Partial<TimeSliderControlEmbeddableInput>,
embeddable?: ControlEmbeddable<TimeSliderControlEmbeddableInput>
) => {
if (
embeddable &&
((newInput.fieldName && !deepEqual(newInput.fieldName, embeddable.getInput().fieldName)) ||
(newInput.dataViewId && !deepEqual(newInput.dataViewId, embeddable.getInput().dataViewId)))
) {
// if the field name or data view id has changed in this editing session, selected options are invalid, so reset them.
newInput.value = undefined;
}
return newInput;
};
public isFieldCompatible = (dataControlField: DataControlField) => {
if (dataControlField.field.type === 'date') {
dataControlField.compatibleControlTypes.push(this.type);
}
};
public isEditable = () => Promise.resolve(false);
public getDisplayName = () => TimeSliderStrings.getDisplayName();
public getIconType = () => 'clock';
public getDescription = () => TimeSliderStrings.getDescription();
public inject = createOptionsListInject();
public extract = createOptionsListExtract();
}

View file

@ -1,20 +0,0 @@
/*
* 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 { PayloadAction } from '@reduxjs/toolkit';
import { WritableDraft } from 'immer/dist/types/types-external';
import { TimeSliderReduxState } from './types';
export const timeSliderReducers = {
selectRange: (
state: WritableDraft<TimeSliderReduxState>,
action: PayloadAction<[number | null, number | null]>
) => {
state.explicitInput.value = action.payload;
},
};

View file

@ -1,46 +0,0 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const TimeSliderStrings = {
getDisplayName: () =>
i18n.translate('controls.timeSlider.displayName', {
defaultMessage: 'Time slider',
}),
getDescription: () =>
i18n.translate('controls.timeSlider.description', {
defaultMessage: 'Add a slider for selecting a time range',
}),
editor: {
getDataViewTitle: () =>
i18n.translate('controls.timeSlider.editor.dataViewTitle', {
defaultMessage: 'Data view',
}),
getNoDataViewTitle: () =>
i18n.translate('controls.timeSlider.editor.noDataViewTitle', {
defaultMessage: 'Select data view',
}),
getFieldTitle: () =>
i18n.translate('controls.timeSlider.editor.fieldTitle', {
defaultMessage: 'Field',
}),
},
resetButton: {
getLabel: () =>
i18n.translate('controls.timeSlider.resetButton.label', {
defaultMessage: 'Reset selections',
}),
},
noDocumentsPopover: {
getLabel: () =>
i18n.translate('controls.timeSlider.noDocuments.label', {
defaultMessage: 'There were no documents found. Range selection unavailable.',
}),
},
};

View file

@ -1,30 +0,0 @@
/*
* 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 { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public';
import { ControlOutput } from '../../types';
import { TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types';
export * from '../../../common/control_types/time_slider/types';
// Component state is only used by public components.
export interface TimeSliderSubjectState {
range?: {
min?: number;
max?: number;
};
loading: boolean;
}
// public only - redux embeddable state type
export type TimeSliderReduxState = ReduxEmbeddableState<
TimeSliderControlEmbeddableInput,
ControlOutput,
TimeSliderSubjectState
>;

View file

@ -24,12 +24,7 @@ export type {
ControlInput,
} from '../common/types';
export {
CONTROL_GROUP_TYPE,
OPTIONS_LIST_CONTROL,
RANGE_SLIDER_CONTROL,
TIME_SLIDER_CONTROL,
} from '../common';
export { CONTROL_GROUP_TYPE, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '../common';
export {
ControlGroupContainer,

View file

@ -14,7 +14,6 @@ import {
CONTROL_GROUP_TYPE,
OPTIONS_LIST_CONTROL,
RANGE_SLIDER_CONTROL,
// TIME_SLIDER_CONTROL,
} from '.';
import { OptionsListEmbeddableFactory, OptionsListEmbeddableInput } from './options_list';
import { RangeSliderEmbeddableFactory, RangeSliderEmbeddableInput } from './range_slider';
@ -29,13 +28,6 @@ import {
ControlInput,
} from './types';
/*
import {
TimesliderEmbeddableFactory,
TimeSliderControlEmbeddableInput,
} from './control_types/time_slider';
*/
export class ControlsPlugin
implements
Plugin<
@ -101,22 +93,6 @@ export class ControlsPlugin
rangeSliderFactory
);
registerControlType(rangeSliderFactory);
// Time Slider Control Factory Setup
/* Temporary disabling Time Slider
const timeSliderFactoryDef = new TimesliderEmbeddableFactory();
const timeSliderFactory = embeddable.registerEmbeddableFactory(
TIME_SLIDER_CONTROL,
timeSliderFactoryDef
)();
this.transferEditorFunctions<TimeSliderControlEmbeddableInput>(
timeSliderFactoryDef,
timeSliderFactory
);
registerControlType(timeSliderFactory);
*/
});
return {

View file

@ -24,7 +24,7 @@ const rawControlAttributes2: RawControlGroupAttributes = {
controlStyle: 'oneLine',
chainingSystem: 'NONE',
panelsJSON:
'{"9cf90205-e94d-43c9-a3aa-45f359a7522f":{"order":0,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceKilometers","fieldName":"DistanceKilometers","id":"9cf90205-e94d-43c9-a3aa-45f359a7522f","enhancements":{}}},"b47916fd-fc03-4dcd-bef1-5c3b7a315723":{"order":1,"width":"auto","type":"timeSlider","explicitInput":{"title":"timestamp","fieldName":"timestamp","id":"b47916fd-fc03-4dcd-bef1-5c3b7a315723","enhancements":{}}},"f6b076c6-9ef5-483e-b08d-d313d60d4b8c":{"order":2,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceMiles","fieldName":"DistanceMiles","id":"f6b076c6-9ef5-483e-b08d-d313d60d4b8c","enhancements":{}}}}',
'{"9cf90205-e94d-43c9-a3aa-45f359a7522f":{"order":0,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceKilometers","fieldName":"DistanceKilometers","id":"9cf90205-e94d-43c9-a3aa-45f359a7522f","enhancements":{}}},"f6b076c6-9ef5-483e-b08d-d313d60d4b8c":{"order":2,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceMiles","fieldName":"DistanceMiles","id":"f6b076c6-9ef5-483e-b08d-d313d60d4b8c","enhancements":{}}}}',
ignoreParentSettingsJSON:
'{"ignoreFilters":true,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}',
};
@ -34,7 +34,7 @@ const rawControlAttributes3: RawControlGroupAttributes = {
controlStyle: 'oneLine',
chainingSystem: 'HIERARCHICAL',
panelsJSON:
'{"9cf90205-e94d-43c9-a3aa-45f359a7522f":{"order":0,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceKilometers","fieldName":"DistanceKilometers","id":"9cf90205-e94d-43c9-a3aa-45f359a7522f","enhancements":{}}},"b47916fd-fc03-4dcd-bef1-5c3b7a315723":{"order":1,"width":"auto","type":"timeSlider","explicitInput":{"title":"timestamp","fieldName":"timestamp","id":"b47916fd-fc03-4dcd-bef1-5c3b7a315723","enhancements":{}}},"ee325e9e-6ec1-41f9-953f-423d59850d44":{"order":2,"width":"auto","type":"optionsListControl","explicitInput":{"title":"Carrier","fieldName":"Carrier","id":"ee325e9e-6ec1-41f9-953f-423d59850d44","enhancements":{}}},"cb0f5fcd-9ad9-4d4a-b489-b75bd060399b":{"order":3,"width":"auto","type":"optionsListControl","explicitInput":{"title":"DestCityName","fieldName":"DestCityName","id":"cb0f5fcd-9ad9-4d4a-b489-b75bd060399b","enhancements":{}}}}',
'{"9cf90205-e94d-43c9-a3aa-45f359a7522f":{"order":0,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceKilometers","fieldName":"DistanceKilometers","id":"9cf90205-e94d-43c9-a3aa-45f359a7522f","enhancements":{}}},"ee325e9e-6ec1-41f9-953f-423d59850d44":{"order":2,"width":"auto","type":"optionsListControl","explicitInput":{"title":"Carrier","fieldName":"Carrier","id":"ee325e9e-6ec1-41f9-953f-423d59850d44","enhancements":{}}},"cb0f5fcd-9ad9-4d4a-b489-b75bd060399b":{"order":3,"width":"auto","type":"optionsListControl","explicitInput":{"title":"DestCityName","fieldName":"DestCityName","id":"cb0f5fcd-9ad9-4d4a-b489-b75bd060399b","enhancements":{}}}}',
ignoreParentSettingsJSON:
'{"ignoreFilters":false,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}',
};
@ -97,7 +97,7 @@ describe('Control group telemetry function', () => {
});
test('counts all telemetry over multiple runs', () => {
expect(finalTelemetry.total).toBe(10);
expect(finalTelemetry.total).toBe(8);
});
test('counts control types over multiple runs.', () => {
@ -110,10 +110,6 @@ describe('Control group telemetry function', () => {
details: {},
total: 3,
},
timeSlider: {
details: {},
total: 2,
},
});
});

View file

@ -1,22 +0,0 @@
/*
* 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 { EmbeddableRegistryDefinition } from '@kbn/embeddable-plugin/server';
import { TIME_SLIDER_CONTROL } from '../../../common';
import {
createTimeSliderExtract,
createTimeSliderInject,
} from '../../../common/control_types/time_slider/time_slider_persistable_state';
export const timeSliderPersistableStateServiceFactory = (): EmbeddableRegistryDefinition => {
return {
id: TIME_SLIDER_CONTROL,
extract: createTimeSliderExtract(),
inject: createTimeSliderInject(),
};
};

View file

@ -14,7 +14,6 @@ import { setupOptionsListSuggestionsRoute } from './options_list/options_list_su
import { controlGroupContainerPersistableStateServiceFactory } from './control_group/control_group_container_factory';
import { optionsListPersistableStateServiceFactory } from './options_list/options_list_embeddable_factory';
import { rangeSliderPersistableStateServiceFactory } from './range_slider/range_slider_embeddable_factory';
// import { timeSliderPersistableStateServiceFactory } from './control_types/time_slider/time_slider_embeddable_factory';
interface SetupDeps {
embeddable: EmbeddableSetup;
@ -24,14 +23,11 @@ interface SetupDeps {
export class ControlsPlugin implements Plugin<object, object, SetupDeps> {
public setup(core: CoreSetup, { embeddable, unifiedSearch }: SetupDeps) {
embeddable.registerEmbeddableFactory(optionsListPersistableStateServiceFactory());
embeddable.registerEmbeddableFactory(rangeSliderPersistableStateServiceFactory());
// Temporary disabling Time Slider
// embeddable.registerEmbeddableFactory(timeSliderPersistableStateServiceFactory());
embeddable.registerEmbeddableFactory(
controlGroupContainerPersistableStateServiceFactory(embeddable)
);
embeddable.registerEmbeddableFactory(optionsListPersistableStateServiceFactory());
embeddable.registerEmbeddableFactory(rangeSliderPersistableStateServiceFactory());
setupOptionsListSuggestionsRoute(core, unifiedSearch.autocomplete.getAutocompleteSettings);
return {};

View file

@ -6,11 +6,7 @@
* Side Public License, v 1.
*/
import {
OPTIONS_LIST_CONTROL,
RANGE_SLIDER_CONTROL,
TIME_SLIDER_CONTROL,
} from '@kbn/controls-plugin/common';
import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '@kbn/controls-plugin/common';
import { FtrProviderContext } from '../../../ftr_provider_context';
@ -49,12 +45,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
};
const replaceWithTimeSlider = async (controlId: string) => {
await changeFieldType(controlId, '@timestamp', TIME_SLIDER_CONTROL);
await testSubjects.waitForDeleted('timeSlider-loading-spinner');
await dashboardControls.verifyControlType(controlId, 'timeSlider');
};
describe('Replacing controls', async () => {
let controlId: string;
@ -89,12 +79,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('with range slider', async () => {
await replaceWithRangeSlider(controlId);
});
/** Because the time slider is temporarily disabled as of https://github.com/elastic/kibana/pull/130978,
** I simply skipped all time slider tests for now :) **/
it.skip('with time slider', async () => {
await replaceWithTimeSlider(controlId);
});
});
describe('Replace range slider', async () => {
@ -116,35 +100,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('with options list', async () => {
await replaceWithOptionsList(controlId);
});
it.skip('with time slider', async () => {
await replaceWithTimeSlider(controlId);
});
});
describe.skip('Replace time slider', async () => {
beforeEach(async () => {
await dashboardControls.clearAllControls();
await dashboardControls.createControl({
controlType: TIME_SLIDER_CONTROL,
dataViewTitle: 'animals-*',
fieldName: '@timestamp',
});
await testSubjects.waitForDeleted('timeSlider-loading-spinner');
controlId = (await dashboardControls.getAllControlIds())[0];
});
afterEach(async () => {
await dashboard.clearUnsavedChanges();
});
it('with options list', async () => {
await replaceWithOptionsList(controlId);
});
it('with range slider', async () => {
await replaceWithRangeSlider(controlId);
});
});
});
}

View file

@ -390,13 +390,6 @@
"controls.rangeSlider.popover.clearRangeTitle": "Effacer la plage",
"controls.rangeSlider.popover.noAvailableDataHelpText": "Il n'y a aucune donnée à afficher. Ajustez la plage temporelle et les filtres.",
"controls.rangeSlider.popover.noDataHelpText": "La plage sélectionnée n'a généré aucune donnée. Aucun filtre n'a été appliqué.",
"controls.timeSlider.description": "Ajouter un curseur pour la sélection d'une plage temporelle",
"controls.timeSlider.displayName": "Curseur temporel",
"controls.timeSlider.editor.dataViewTitle": "Vue de données",
"controls.timeSlider.editor.fieldTitle": "Champ",
"controls.timeSlider.editor.noDataViewTitle": "Sélectionner la vue de données",
"controls.timeSlider.noDocuments.label": "Aucun document n'a été trouvé. Sélection de plage non disponible.",
"controls.timeSlider.resetButton.label": "Réinitialiser les sélections",
"core.chrome.browserDeprecationWarning": "La prise en charge d'Internet Explorer sera abandonnée dans les futures versions de ce logiciel. Veuillez consulter le site {link}.",
"core.deprecations.deprecations.fetchFailedMessage": "Impossible d'extraire les informations de déclassement pour le plug-in {domainId}.",
"core.deprecations.deprecations.fetchFailedTitle": "Impossible d'extraire les déclassements pour {domainId}",

View file

@ -390,13 +390,6 @@
"controls.rangeSlider.popover.clearRangeTitle": "範囲を消去",
"controls.rangeSlider.popover.noAvailableDataHelpText": "表示するデータがありません。時間範囲とフィルターを調整します。",
"controls.rangeSlider.popover.noDataHelpText": "選択された範囲にはデータがありません。フィルターが適用されませんでした。",
"controls.timeSlider.description": "時間範囲を選択するためのスライダーを追加",
"controls.timeSlider.displayName": "時間スライダー",
"controls.timeSlider.editor.dataViewTitle": "データビュー",
"controls.timeSlider.editor.fieldTitle": "フィールド",
"controls.timeSlider.editor.noDataViewTitle": "データビューを選択",
"controls.timeSlider.noDocuments.label": "ドキュメントが見つかりませんでした。 範囲選択を使用できません。",
"controls.timeSlider.resetButton.label": "選択項目をリセット",
"core.chrome.browserDeprecationWarning": "このソフトウェアの将来のバージョンでは、Internet Explorerのサポートが削除されます。{link}をご確認ください。",
"core.deprecations.deprecations.fetchFailedMessage": "プラグイン{domainId}の廃止予定情報を取得できません。",
"core.deprecations.deprecations.fetchFailedTitle": "{domainId}の廃止予定を取得できませんでした",

View file

@ -390,13 +390,6 @@
"controls.rangeSlider.popover.clearRangeTitle": "清除范围",
"controls.rangeSlider.popover.noAvailableDataHelpText": "没有可显示的数据。调整时间范围和筛选。",
"controls.rangeSlider.popover.noDataHelpText": "选定范围未生成任何数据。未应用任何筛选。",
"controls.timeSlider.description": "添加用于选择时间范围的滑块",
"controls.timeSlider.displayName": "时间滑块",
"controls.timeSlider.editor.dataViewTitle": "数据视图",
"controls.timeSlider.editor.fieldTitle": "字段",
"controls.timeSlider.editor.noDataViewTitle": "选择数据视图",
"controls.timeSlider.noDocuments.label": "找不到文档。 范围选择不可用。",
"controls.timeSlider.resetButton.label": "重置选择",
"core.chrome.browserDeprecationWarning": "本软件的未来版本将放弃对 Internet Explorer 的支持,请查看{link}。",
"core.deprecations.deprecations.fetchFailedMessage": "无法提取插件 {domainId} 的弃用信息。",
"core.deprecations.deprecations.fetchFailedTitle": "无法提取 {domainId} 的弃用信息",