mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[maps] use control group time slider (#146486)
PR replaces Maps time slider implementation with control time slider <img width="600" alt="Screen Shot 2022-11-28 at 3 57 24 PM" src="https://user-images.githubusercontent.com/373691/204398771-54b36093-f0c9-4e3b-8637-ecc8b6dd2429.png"> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
27077dfca0
commit
8695086d3d
12 changed files with 95 additions and 429 deletions
|
@ -49,6 +49,7 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
|
||||
private getTimezone: ControlsSettingsService['getTimezone'];
|
||||
private timefilter: ControlsDataService['timefilter'];
|
||||
private prevTimeRange: TimeRange | undefined;
|
||||
private readonly waitForControlOutputConsumersToLoad$;
|
||||
|
||||
private reduxEmbeddableTools: ReduxEmbeddableTools<
|
||||
|
@ -129,9 +130,9 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
return;
|
||||
}
|
||||
|
||||
const nextBounds = this.timeRangeToBounds(input.timeRange);
|
||||
const { actions, dispatch, getState } = this.reduxEmbeddableTools;
|
||||
if (!_.isEqual(nextBounds, getState().componentState.timeRangeBounds)) {
|
||||
if (!_.isEqual(input.timeRange, this.prevTimeRange)) {
|
||||
const { actions, dispatch } = this.reduxEmbeddableTools;
|
||||
const nextBounds = this.timeRangeToBounds(input.timeRange);
|
||||
const ticks = getTicks(nextBounds[FROM_INDEX], nextBounds[TO_INDEX], this.getTimezone());
|
||||
dispatch(
|
||||
actions.setTimeRangeBounds({
|
||||
|
@ -145,6 +146,7 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
}
|
||||
|
||||
private syncWithTimeRange() {
|
||||
this.prevTimeRange = this.getInput().timeRange;
|
||||
const { actions, dispatch, getState } = this.reduxEmbeddableTools;
|
||||
const stepSize = getState().componentState.stepSize;
|
||||
const timesliceStartAsPercentageOfTimeRange =
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "maps"],
|
||||
"requiredPlugins": [
|
||||
"controls",
|
||||
"unifiedSearch",
|
||||
"lens",
|
||||
"licensing",
|
||||
|
|
|
@ -9,7 +9,11 @@ import { AnyAction } from 'redux';
|
|||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { connect } from 'react-redux';
|
||||
import { MapContainer } from './map_container';
|
||||
import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors';
|
||||
import {
|
||||
getFlyoutDisplay,
|
||||
getIsFullScreen,
|
||||
getIsTimesliderOpen,
|
||||
} from '../../selectors/ui_selectors';
|
||||
import { cancelAllInFlightRequests, exitFullScreen } from '../../actions';
|
||||
import {
|
||||
areLayersLoaded,
|
||||
|
@ -22,6 +26,7 @@ import { MapStoreState } from '../../reducers/store';
|
|||
|
||||
function mapStateToProps(state: MapStoreState) {
|
||||
return {
|
||||
isTimesliderOpen: getIsTimesliderOpen(state),
|
||||
areLayersLoaded: areLayersLoaded(state),
|
||||
flyoutDisplay: getFlyoutDisplay(state),
|
||||
isFullScreen: getIsFullScreen(state),
|
||||
|
|
|
@ -13,7 +13,6 @@ import uuid from 'uuid/v4';
|
|||
import { Filter } from '@kbn/es-query';
|
||||
import { ActionExecutionContext, Action } from '@kbn/ui-actions-plugin/public';
|
||||
import { Observable } from 'rxjs';
|
||||
import moment from 'moment';
|
||||
import { ExitFullScreenButton } from '@kbn/shared-ux-button-exit-full-screen';
|
||||
import { MBMap } from '../mb_map';
|
||||
import { RightSideControls } from '../right_side_controls';
|
||||
|
@ -21,7 +20,7 @@ import { Timeslider } from '../timeslider';
|
|||
import { ToolbarOverlay } from '../toolbar_overlay';
|
||||
import { EditLayerPanel } from '../edit_layer_panel';
|
||||
import { AddLayerPanel } from '../add_layer_panel';
|
||||
import { getData, isScreenshotMode } from '../../kibana_services';
|
||||
import { isScreenshotMode } from '../../kibana_services';
|
||||
import { RawValue } from '../../../common/constants';
|
||||
import { FLYOUT_STATE } from '../../reducers/ui';
|
||||
import { MapSettings } from '../../../common/descriptor_types';
|
||||
|
@ -41,6 +40,7 @@ export interface Props {
|
|||
exitFullScreen: () => void;
|
||||
flyoutDisplay: FLYOUT_STATE;
|
||||
isFullScreen: boolean;
|
||||
isTimesliderOpen: boolean;
|
||||
indexPatternIds: string[];
|
||||
mapInitError: string | null | undefined;
|
||||
renderTooltipContent?: RenderToolTipContent;
|
||||
|
@ -154,13 +154,6 @@ export class MapContainer extends Component<Props, State> {
|
|||
}, 5000);
|
||||
};
|
||||
|
||||
_updateGlobalTimeRange(data: number[]) {
|
||||
getData().query.timefilter.timefilter.setTime({
|
||||
from: moment(data[0]).toISOString(),
|
||||
to: moment(data[1]).toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
addFilters,
|
||||
|
@ -241,13 +234,10 @@ export class MapContainer extends Component<Props, State> {
|
|||
/>
|
||||
)}
|
||||
<RightSideControls />
|
||||
{this.props.isTimesliderOpen && (
|
||||
<Timeslider waitForTimesliceToLoad$={this.props.waitUntilTimeLayersLoad$} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
||||
<Timeslider
|
||||
waitForTimesliceToLoad$={this.props.waitUntilTimeLayersLoad$}
|
||||
updateGlobalTimeRange={this._updateGlobalTimeRange.bind(this)}
|
||||
/>
|
||||
|
||||
<EuiFlexItem
|
||||
className={classNames('mapMapLayerPanel', {
|
||||
'mapMapLayerPanel-isVisible': !!flyoutPanel,
|
||||
|
|
|
@ -1,66 +1,14 @@
|
|||
$timesliderWidth: 650px;
|
||||
|
||||
.mapTimeslider {
|
||||
@include euiBottomShadowLarge;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: $euiSize;
|
||||
// we could center with translateX but this would impact the animation
|
||||
margin-left: -($timesliderWidth / 2);
|
||||
width: $timesliderWidth;
|
||||
z-index: 99999;
|
||||
background: $euiColorEmptyShade;
|
||||
padding: $euiSizeL $euiSize;
|
||||
border-radius: $euiSize;
|
||||
}
|
||||
|
||||
.mapTimeslider__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& + & {
|
||||
margin-top: $euiSizeS;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding: 0 $euiSize;
|
||||
}
|
||||
}
|
||||
|
||||
.mapTimeslider__close {
|
||||
position: absolute;
|
||||
top: $euiSizeXS;
|
||||
right: $euiSizeXS;
|
||||
}
|
||||
left: calc(50% - 400px);
|
||||
bottom: $euiSize;
|
||||
z-index: 99999;
|
||||
|
||||
.mapTimeslider__timeWindow {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
margin-right: $euiSizeS;
|
||||
font-size: $euiFontSizeS;
|
||||
}
|
||||
|
||||
.mapTimeslider__controls {
|
||||
margin-left: $euiSizeS;
|
||||
|
||||
.euiButtonIcon + .euiButtonIcon {
|
||||
margin-left: $euiSizeXS;
|
||||
.controlFrame__formControlLayout {
|
||||
@include euiBottomShadowLarge;
|
||||
}
|
||||
}
|
||||
|
||||
.mapTimeslider__innerPanel {
|
||||
display: inline-flex;
|
||||
// background: $euiColorLightestShade;
|
||||
border-radius: $euiBorderRadiusSmall;
|
||||
padding: $euiSizeXS;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mapTimeslider__playButton {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.mapTimeslider--animation {
|
||||
animation: mapAnimationPulse .6s ease-in-out;
|
||||
}
|
||||
}
|
|
@ -9,25 +9,20 @@ import { AnyAction } from 'redux';
|
|||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { connect } from 'react-redux';
|
||||
import { Timeslider } from './timeslider';
|
||||
import { closeTimeslider, setQuery } from '../../actions';
|
||||
import { setQuery } from '../../actions';
|
||||
import { getTimeFilters } from '../../selectors/map_selectors';
|
||||
import { getIsTimesliderOpen } from '../../selectors/ui_selectors';
|
||||
import { MapStoreState } from '../../reducers/store';
|
||||
import { Timeslice } from '../../../common/descriptor_types';
|
||||
|
||||
function mapStateToProps(state: MapStoreState) {
|
||||
return {
|
||||
isTimesliderOpen: getIsTimesliderOpen(state),
|
||||
timeRange: getTimeFilters(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) {
|
||||
return {
|
||||
closeTimeslider: () => {
|
||||
dispatch(closeTimeslider());
|
||||
},
|
||||
setTimeslice: (timeslice: Timeslice) => {
|
||||
setTimeslice: (timeslice?: Timeslice) => {
|
||||
dispatch(
|
||||
setQuery({
|
||||
forceRefresh: false,
|
||||
|
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getInterval } from './time_utils';
|
||||
|
||||
describe('getInterval', () => {
|
||||
test('should provide interval of 1 day for 7 day range', () => {
|
||||
expect(getInterval(1617630946622, 1618235746622)).toBe(86400000);
|
||||
});
|
||||
|
||||
test('should provide interval of 3 hours for 24 hour range', () => {
|
||||
expect(getInterval(1618150382531, 1618236782531)).toBe(10800000);
|
||||
});
|
||||
|
||||
test('should provide interval of 90 minues for 12 hour range', () => {
|
||||
expect(getInterval(1618193892632, 1618237092632)).toBe(5400000);
|
||||
});
|
||||
|
||||
test('should provide interval of 30 minues for 4 hour range', () => {
|
||||
expect(getInterval(1618222509189, 1618236909189)).toBe(1800000);
|
||||
});
|
||||
|
||||
test('should provide interval of 10 minues for 1 hour range', () => {
|
||||
expect(getInterval(1618233266459, 1618236866459)).toBe(600000);
|
||||
});
|
||||
});
|
|
@ -1,84 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import { EuiRangeTick } from '@elastic/eui/src/components/form/range/range_ticks';
|
||||
import { calcAutoIntervalNear } from '@kbn/data-plugin/common';
|
||||
import { getUiSettings } from '../../kibana_services';
|
||||
|
||||
function getTimezone() {
|
||||
const detectedTimezone = moment.tz.guess();
|
||||
const dateFormatTZ = getUiSettings().get('dateFormat:tz', 'Browser');
|
||||
|
||||
return dateFormatTZ === 'Browser' ? detectedTimezone : dateFormatTZ;
|
||||
}
|
||||
|
||||
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 epochToKbnDateFormat(epoch: number): string {
|
||||
const dateFormat = getUiSettings().get('dateFormat', 'MMM D, YYYY @ HH:mm:ss.SSS');
|
||||
const timezone = getTimezone();
|
||||
return moment.tz(epoch, timezone).format(dateFormat);
|
||||
}
|
||||
|
||||
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 function getTicks(min: number, max: number, interval: number): EuiRangeTick[] {
|
||||
const format = getScaledDateFormat(interval);
|
||||
const timezone = getTimezone();
|
||||
|
||||
let tick = Math.ceil(min / interval) * interval;
|
||||
const ticks: EuiRangeTick[] = [];
|
||||
while (tick < max) {
|
||||
ticks.push({
|
||||
value: tick,
|
||||
label: moment.tz(tick, timezone).format(format),
|
||||
});
|
||||
tick += interval;
|
||||
}
|
||||
|
||||
return ticks;
|
||||
}
|
|
@ -7,248 +7,105 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { EuiButtonIcon, EuiDualRange, EuiText } from '@elastic/eui';
|
||||
import { EuiRangeTick } from '@elastic/eui/src/components/form/range/range_ticks';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
ControlGroupContainer,
|
||||
type ControlGroupInput,
|
||||
type controlGroupInputBuilder,
|
||||
LazyControlGroupRenderer,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import { withSuspense } from '@kbn/presentation-util-plugin/public';
|
||||
import { first } from 'rxjs/operators';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { epochToKbnDateFormat, getInterval, getTicks } from './time_utils';
|
||||
import { getTimeFilter } from '../../kibana_services';
|
||||
import { Timeslice } from '../../../common/descriptor_types';
|
||||
|
||||
const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer);
|
||||
|
||||
export interface Props {
|
||||
closeTimeslider: () => void;
|
||||
setTimeslice: (timeslice: Timeslice) => void;
|
||||
isTimesliderOpen: boolean;
|
||||
setTimeslice: (timeslice?: Timeslice) => void;
|
||||
timeRange: TimeRange;
|
||||
waitForTimesliceToLoad$: Observable<void>;
|
||||
updateGlobalTimeRange: (timeslice: number[]) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isPaused: boolean;
|
||||
max: number;
|
||||
min: number;
|
||||
range: number;
|
||||
timeslice: [number, number];
|
||||
ticks: EuiRangeTick[];
|
||||
}
|
||||
|
||||
function prettyPrintTimeslice(timeslice: [number, number]) {
|
||||
return `${epochToKbnDateFormat(timeslice[0])} - ${epochToKbnDateFormat(timeslice[1])}`;
|
||||
}
|
||||
|
||||
// Why Timeslider and KeyedTimeslider?
|
||||
// Using react 'key' property to ensure new KeyedTimeslider instance whenever props.timeRange changes
|
||||
export function Timeslider(props: Props) {
|
||||
return props.isTimesliderOpen ? (
|
||||
<KeyedTimeslider key={`${props.timeRange.from}-${props.timeRange.to}`} {...props} />
|
||||
) : null;
|
||||
}
|
||||
|
||||
class KeyedTimeslider extends Component<Props, State> {
|
||||
export class Timeslider extends Component<Props, {}> {
|
||||
private _isMounted: boolean = false;
|
||||
private _timeoutId: number | undefined;
|
||||
private _subscription: Subscription | undefined;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const timeRangeBounds = getTimeFilter().calculateBounds(props.timeRange);
|
||||
if (timeRangeBounds.min === undefined || timeRangeBounds.max === undefined) {
|
||||
throw new Error(
|
||||
'Unable to create Timeslider component, timeRangeBounds min or max are undefined'
|
||||
);
|
||||
}
|
||||
const min = timeRangeBounds.min.valueOf();
|
||||
const max = timeRangeBounds.max.valueOf();
|
||||
const interval = getInterval(min, max);
|
||||
const timeslice: [number, number] = [min, max];
|
||||
|
||||
this.state = {
|
||||
isPaused: true,
|
||||
max,
|
||||
min,
|
||||
range: interval,
|
||||
ticks: getTicks(min, max, interval),
|
||||
timeslice,
|
||||
};
|
||||
}
|
||||
private _controlGroup?: ControlGroupContainer | undefined;
|
||||
private readonly _subscriptions = new Subscription();
|
||||
|
||||
componentWillUnmount() {
|
||||
this._onPause();
|
||||
this._isMounted = false;
|
||||
this._subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (
|
||||
this._controlGroup &&
|
||||
!_.isEqual(this._controlGroup.getInput().timeRange, this.props.timeRange)
|
||||
) {
|
||||
this._controlGroup.updateInput({
|
||||
timeRange: this.props.timeRange,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
// auto-select range between first tick and second tick
|
||||
this._onChange([this.state.ticks[0].value, this.state.ticks[1].value]);
|
||||
}
|
||||
|
||||
_doesTimesliceCoverTimerange() {
|
||||
return this.state.timeslice[0] === this.state.min && this.state.timeslice[1] === this.state.max;
|
||||
}
|
||||
|
||||
_onDualControlChange = (value: [number | string, number | string]) => {
|
||||
this.setState({ range: (value[1] as number) - (value[0] as number) }, () => {
|
||||
this._onChange(value as [number, number]);
|
||||
});
|
||||
_getInitialInput = async (
|
||||
initialInput: Partial<ControlGroupInput>,
|
||||
builder: typeof controlGroupInputBuilder
|
||||
) => {
|
||||
builder.addTimeSliderControl(initialInput);
|
||||
return {
|
||||
...initialInput,
|
||||
viewMode: ViewMode.VIEW,
|
||||
timeRange: this.props.timeRange,
|
||||
};
|
||||
};
|
||||
|
||||
_onChange = (value: [number, number]) => {
|
||||
this.setState({
|
||||
timeslice: value,
|
||||
});
|
||||
this._propagateChange(value);
|
||||
};
|
||||
|
||||
_onNext = () => {
|
||||
const from =
|
||||
this._doesTimesliceCoverTimerange() || this.state.timeslice[1] === this.state.max
|
||||
? this.state.ticks[0].value
|
||||
: this.state.timeslice[1];
|
||||
const to = from + this.state.range;
|
||||
this._onChange([from, to <= this.state.max ? to : this.state.max]);
|
||||
};
|
||||
|
||||
_onPrevious = () => {
|
||||
const to =
|
||||
this._doesTimesliceCoverTimerange() || this.state.timeslice[0] === this.state.min
|
||||
? this.state.ticks[this.state.ticks.length - 1].value
|
||||
: this.state.timeslice[0];
|
||||
const from = to - this.state.range;
|
||||
this._onChange([from < this.state.min ? this.state.min : from, to]);
|
||||
};
|
||||
|
||||
_propagateChange = _.debounce((value: [number, number]) => {
|
||||
if (this._isMounted) {
|
||||
this.props.setTimeslice({ from: value[0], to: value[1] });
|
||||
_onLoadComplete = (controlGroup: ControlGroupContainer) => {
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
_onPlay = () => {
|
||||
this.setState({ isPaused: false });
|
||||
this._playNextFrame();
|
||||
this._controlGroup = controlGroup;
|
||||
this._subscriptions.add(
|
||||
this._controlGroup
|
||||
.getOutput$()
|
||||
.pipe(
|
||||
distinctUntilChanged(({ timeslice: timesliceA }, { timeslice: timesliceB }) =>
|
||||
_.isEqual(timesliceA, timesliceB)
|
||||
)
|
||||
)
|
||||
.subscribe(({ timeslice }) => {
|
||||
// use waitForTimesliceToLoad$ observable to wait until next frame loaded
|
||||
// .pipe(first()) waits until the first value is emitted from an observable and then automatically unsubscribes
|
||||
this.props.waitForTimesliceToLoad$.pipe(first()).subscribe(() => {
|
||||
this._controlGroup!.anyControlOutputConsumerLoading$.next(false);
|
||||
});
|
||||
|
||||
this.props.setTimeslice(
|
||||
timeslice === undefined
|
||||
? undefined
|
||||
: {
|
||||
from: timeslice[0],
|
||||
to: timeslice[1],
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
_onPause = () => {
|
||||
this.setState({ isPaused: true });
|
||||
if (this._subscription) {
|
||||
this._subscription.unsubscribe();
|
||||
this._subscription = undefined;
|
||||
}
|
||||
if (this._timeoutId) {
|
||||
clearTimeout(this._timeoutId);
|
||||
this._timeoutId = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
_playNextFrame() {
|
||||
// advance to next frame
|
||||
this._onNext();
|
||||
|
||||
// use waitForTimesliceToLoad$ observable to wait until next frame loaded
|
||||
// .pipe(first()) waits until the first value is emitted from an observable and then automatically unsubscribes
|
||||
this._subscription = this.props.waitForTimesliceToLoad$.pipe(first()).subscribe(() => {
|
||||
if (this.state.isPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
// use timeout to display frame for small time period before moving to next frame
|
||||
this._timeoutId = window.setTimeout(() => {
|
||||
if (this.state.isPaused) {
|
||||
return;
|
||||
}
|
||||
this._playNextFrame();
|
||||
}, 1750);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mapTimeslider mapTimeslider--animation">
|
||||
<div className="mapTimeslider__row">
|
||||
<EuiButtonIcon
|
||||
onClick={this.props.closeTimeslider}
|
||||
iconType="cross"
|
||||
color="text"
|
||||
className="mapTimeslider__close"
|
||||
aria-label={i18n.translate('xpack.maps.timeslider.closeLabel', {
|
||||
defaultMessage: 'Close timeslider',
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className="mapTimeslider__timeWindow">
|
||||
<EuiText size="s">{prettyPrintTimeslice(this.state.timeslice)}</EuiText>
|
||||
</div>
|
||||
|
||||
<EuiButtonIcon
|
||||
onClick={() => {
|
||||
this.props.updateGlobalTimeRange(this.state.timeslice);
|
||||
}}
|
||||
iconType="calendar"
|
||||
aria-label={i18n.translate('xpack.maps.timeslider.setGlobalTime', {
|
||||
defaultMessage: 'Set global time to {timeslice}',
|
||||
values: { timeslice: prettyPrintTimeslice(this.state.timeslice) },
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.timeslider.setGlobalTime', {
|
||||
defaultMessage: 'Set global time to {timeslice}',
|
||||
values: { timeslice: prettyPrintTimeslice(this.state.timeslice) },
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className="mapTimeslider__innerPanel">
|
||||
<div className="mapTimeslider__controls">
|
||||
<EuiButtonIcon
|
||||
onClick={this._onPrevious}
|
||||
iconType="framePrevious"
|
||||
color="text"
|
||||
aria-label={i18n.translate('xpack.maps.timeslider.previousTimeWindowLabel', {
|
||||
defaultMessage: 'Previous time window',
|
||||
})}
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
className="mapTimeslider__playButton"
|
||||
onClick={this.state.isPaused ? this._onPlay : this._onPause}
|
||||
iconType={this.state.isPaused ? 'playFilled' : 'pause'}
|
||||
size="s"
|
||||
display="fill"
|
||||
aria-label={
|
||||
this.state.isPaused
|
||||
? i18n.translate('xpack.maps.timeslider.playLabel', {
|
||||
defaultMessage: 'Play',
|
||||
})
|
||||
: i18n.translate('xpack.maps.timeslider.pauseLabel', {
|
||||
defaultMessage: 'Pause',
|
||||
})
|
||||
}
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
onClick={this._onNext}
|
||||
iconType="frameNext"
|
||||
color="text"
|
||||
aria-label={i18n.translate('xpack.maps.timeslider.nextTimeWindowLabel', {
|
||||
defaultMessage: 'Next time window',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mapTimeslider__row">
|
||||
<EuiDualRange
|
||||
fullWidth={true}
|
||||
value={this.state.timeslice}
|
||||
onChange={this._onDualControlChange}
|
||||
showTicks={true}
|
||||
min={this.state.min}
|
||||
max={this.state.max}
|
||||
step={1}
|
||||
ticks={this.state.ticks}
|
||||
isDraggable
|
||||
/>
|
||||
</div>
|
||||
<ControlGroupRenderer
|
||||
onLoadComplete={this._onLoadComplete}
|
||||
getInitialInput={this._getInitialInput}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18362,7 +18362,6 @@
|
|||
"xpack.maps.tiles.resultsCompleteMsg": "{countPrefix}{count} documents trouvés.",
|
||||
"xpack.maps.tiles.resultsTrimmedMsg": "Résultats limités à {countPrefix}{count} documents.",
|
||||
"xpack.maps.tileStatusTracker.layerErrorMsg": "Impossible de charger {count} tuiles : {tileErrors}",
|
||||
"xpack.maps.timeslider.setGlobalTime": "Définir l’heure globale sur {timeslice}",
|
||||
"xpack.maps.tooltip.joinPropertyTooltipContent": "La clé partagée \"{leftFieldName}\" est reliée à {rightSources}.",
|
||||
"xpack.maps.tooltip.pageNumerText": "{pageNumber} sur {total}",
|
||||
"xpack.maps.tooltipSelector.addLabelWithCount": "Ajouter {count}",
|
||||
|
@ -18937,11 +18936,6 @@
|
|||
"xpack.maps.tileMap.vis.title": "Carte de coordonnées",
|
||||
"xpack.maps.tiles.shapeCountMsg": " Ce nombre est approximatif.",
|
||||
"xpack.maps.tileStatusTracker.tileErrorMsg": "impossible de charger la tuile \"{tileZXYKey}\" : \"{status} {message}\"",
|
||||
"xpack.maps.timeslider.closeLabel": "Fermer le curseur temporel",
|
||||
"xpack.maps.timeslider.nextTimeWindowLabel": "Fenêtre temporelle suivante",
|
||||
"xpack.maps.timeslider.pauseLabel": "Pause",
|
||||
"xpack.maps.timeslider.playLabel": "Lecture",
|
||||
"xpack.maps.timeslider.previousTimeWindowLabel": "Fenêtre temporelle précédente",
|
||||
"xpack.maps.timesliderToggleButton.closeLabel": "Fermer le curseur temporel",
|
||||
"xpack.maps.timesliderToggleButton.openLabel": "Ouvrir le curseur temporel",
|
||||
"xpack.maps.toolbarOverlay.drawBounds.initialGeometryLabel": "limites",
|
||||
|
|
|
@ -18345,7 +18345,6 @@
|
|||
"xpack.maps.tiles.resultsCompleteMsg": "{countPrefix}{count}個のドキュメントが見つかりました。",
|
||||
"xpack.maps.tiles.resultsTrimmedMsg": "結果は{countPrefix}{count}個のドキュメントに制限されています。",
|
||||
"xpack.maps.tileStatusTracker.layerErrorMsg": "{count}個のタイルを読み込めません:{tileErrors}",
|
||||
"xpack.maps.timeslider.setGlobalTime": "グローバル時刻を{timeslice}に設定",
|
||||
"xpack.maps.tooltip.joinPropertyTooltipContent": "共有キー'{leftFieldName}'は{rightSources}と結合されます",
|
||||
"xpack.maps.tooltip.pageNumerText": "{total}ページ中 {pageNumber}ページ",
|
||||
"xpack.maps.tooltipSelector.addLabelWithCount": "{count} の追加",
|
||||
|
@ -18920,11 +18919,6 @@
|
|||
"xpack.maps.tileMap.vis.title": "座標マップ",
|
||||
"xpack.maps.tiles.shapeCountMsg": " この数は近似値です。",
|
||||
"xpack.maps.tileStatusTracker.tileErrorMsg": "タイル'{tileZXYKey}'を読み込めませんでした:'{status} {message}'",
|
||||
"xpack.maps.timeslider.closeLabel": "時間スライダーを閉じる",
|
||||
"xpack.maps.timeslider.nextTimeWindowLabel": "次の時間ウィンドウ",
|
||||
"xpack.maps.timeslider.pauseLabel": "一時停止",
|
||||
"xpack.maps.timeslider.playLabel": "再生",
|
||||
"xpack.maps.timeslider.previousTimeWindowLabel": "前の時間ウィンドウ",
|
||||
"xpack.maps.timesliderToggleButton.closeLabel": "時間スライダーを閉じる",
|
||||
"xpack.maps.timesliderToggleButton.openLabel": "時間スライダーを開く",
|
||||
"xpack.maps.toolbarOverlay.drawBounds.initialGeometryLabel": "境界",
|
||||
|
|
|
@ -18370,7 +18370,6 @@
|
|||
"xpack.maps.tiles.resultsCompleteMsg": "找到 {countPrefix}{count} 个文档。",
|
||||
"xpack.maps.tiles.resultsTrimmedMsg": "结果仅限于 {countPrefix}{count} 个文档。",
|
||||
"xpack.maps.tileStatusTracker.layerErrorMsg": "无法加载 {count} 个磁贴:{tileErrors}",
|
||||
"xpack.maps.timeslider.setGlobalTime": "将全局时间设置为 {timeslice}",
|
||||
"xpack.maps.tooltip.joinPropertyTooltipContent": "共享密钥“{leftFieldName}”与 {rightSources} 联结",
|
||||
"xpack.maps.tooltip.pageNumerText": "第 {pageNumber} 页,共 {total} 页",
|
||||
"xpack.maps.tooltipSelector.addLabelWithCount": "添加 {count} 个",
|
||||
|
@ -18945,11 +18944,6 @@
|
|||
"xpack.maps.tileMap.vis.title": "坐标地图",
|
||||
"xpack.maps.tiles.shapeCountMsg": " 此计数为近似值。",
|
||||
"xpack.maps.tileStatusTracker.tileErrorMsg": "无法加载磁贴“{tileZXYKey}”:“{status} {message}”",
|
||||
"xpack.maps.timeslider.closeLabel": "关闭时间滑块",
|
||||
"xpack.maps.timeslider.nextTimeWindowLabel": "下一时间窗口",
|
||||
"xpack.maps.timeslider.pauseLabel": "暂停",
|
||||
"xpack.maps.timeslider.playLabel": "播放",
|
||||
"xpack.maps.timeslider.previousTimeWindowLabel": "上一时间窗口",
|
||||
"xpack.maps.timesliderToggleButton.closeLabel": "关闭时间滑块",
|
||||
"xpack.maps.timesliderToggleButton.openLabel": "打开时间滑块",
|
||||
"xpack.maps.toolbarOverlay.drawBounds.initialGeometryLabel": "边界",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue