mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
f2c20b7c9f
commit
239def566c
24 changed files with 7 additions and 1677 deletions
|
@ -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 };
|
||||
};
|
||||
};
|
|
@ -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];
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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]}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
→
|
||||
<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" />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
);
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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.',
|
||||
}),
|
||||
},
|
||||
};
|
|
@ -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
|
||||
>;
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
};
|
|
@ -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 {};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}の廃止予定を取得できませんでした",
|
||||
|
|
|
@ -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} 的弃用信息",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue