Files
weave-scope/client/app/scripts/components/time-travel.js
Filip Barl 8f2d47ce4e Time travel redesign (#2651)
* Initial top level control.

* Added the jump buttons.

* Tiny styling adjustments.

* Massive renaming.

* Pause info

* Added slider marks.

* Improved messaging.

* Freeze all updates when paused.

* Repositioned for Configure button.

* Improved the flow.

* Working browsing through slider.

* Small styling.

* Hide time travel button behind the feature flag.

* Fixed actions.

* Elements positioning corner cases.

* Removed nodes delta buffering code.

* Fixed the flow.

* Fixed almost all API call cases.

* Final touches

* Fixed the tests.

* Fix resource view updates when time travelling.

* Added some comments.

* Addressed some of @foot's comments.
2017-07-06 16:06:55 +02:00

225 lines
7.0 KiB
JavaScript

import React from 'react';
import Slider from 'rc-slider';
import moment from 'moment';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { debounce, map } from 'lodash';
import { trackMixpanelEvent } from '../utils/tracking-utils';
import {
jumpToTime,
resumeTime,
timeTravelStartTransition,
} from '../actions/app-actions';
import {
TIMELINE_TICK_INTERVAL,
TIMELINE_DEBOUNCE_INTERVAL,
} from '../constants/timer';
const getTimestampStates = (timestamp) => {
timestamp = timestamp || moment();
return {
sliderValue: moment(timestamp).valueOf(),
inputValue: moment(timestamp).utc().format(),
};
};
const ONE_HOUR_MS = moment.duration(1, 'hour');
const FIVE_MINUTES_MS = moment.duration(5, 'minutes');
class TimeTravel extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
// TODO: Showing a three months of history is quite arbitrary;
// we should instead get some meaningful 'beginning of time' from
// the backend and make the slider show whole active history.
sliderMinValue: moment().subtract(6, 'months').valueOf(),
...getTimestampStates(props.pausedAt),
};
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSliderChange = this.handleSliderChange.bind(this);
this.handleJumpClick = this.handleJumpClick.bind(this);
this.renderMarks = this.renderMarks.bind(this);
this.renderMark = this.renderMark.bind(this);
this.travelTo = this.travelTo.bind(this);
this.debouncedUpdateTimestamp = debounce(
this.updateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL);
this.debouncedTrackSliderChange = debounce(
this.trackSliderChange.bind(this), TIMELINE_DEBOUNCE_INTERVAL);
}
componentDidMount() {
// Force periodic re-renders to update the slider position as time goes by.
this.timer = setInterval(() => { this.forceUpdate(); }, TIMELINE_TICK_INTERVAL);
}
componentWillReceiveProps(props) {
this.setState(getTimestampStates(props.pausedAt));
}
componentWillUnmount() {
clearInterval(this.timer);
this.props.resumeTime();
}
handleSliderChange(timestamp) {
this.travelTo(timestamp, true);
this.debouncedTrackSliderChange();
}
handleInputChange(ev) {
let timestamp = moment(ev.target.value);
this.setState({ inputValue: ev.target.value });
if (timestamp.isValid()) {
timestamp = Math.max(timestamp, this.state.sliderMinValue);
timestamp = Math.min(timestamp, moment().valueOf());
this.travelTo(timestamp, true);
trackMixpanelEvent('scope.time.timestamp.edit', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
}
}
handleJumpClick(millisecondsDelta) {
let timestamp = this.state.sliderValue + millisecondsDelta;
timestamp = Math.max(timestamp, this.state.sliderMinValue);
timestamp = Math.min(timestamp, moment().valueOf());
this.travelTo(timestamp, true);
}
updateTimestamp(timestamp) {
this.props.jumpToTime(moment(timestamp));
}
travelTo(timestamp, debounced = false) {
this.props.timeTravelStartTransition();
this.setState(getTimestampStates(timestamp));
if (debounced) {
this.debouncedUpdateTimestamp(timestamp);
} else {
this.debouncedUpdateTimestamp.cancel();
this.updateTimestamp(timestamp);
}
}
trackSliderChange() {
trackMixpanelEvent('scope.time.slider.change', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
}
renderMark({ timestampValue, label }) {
const sliderMaxValue = moment().valueOf();
const pos = (sliderMaxValue - timestampValue) / (sliderMaxValue - this.state.sliderMinValue);
// Ignore the month marks that are very close to 'Now'
if (label !== 'Now' && pos < 0.05) return null;
const style = { marginLeft: `calc(${(1 - pos) * 100}% - 32px)`, width: '64px' };
return (
<div
style={style}
className="time-travel-markers-tick"
key={timestampValue}>
<span className="vertical-tick" />
<a className="link" onClick={() => this.travelTo(timestampValue)}>{label}</a>
</div>
);
}
renderMarks() {
const { sliderMinValue } = this.state;
const sliderMaxValue = moment().valueOf();
const ticks = [{ timestampValue: sliderMaxValue, label: 'Now' }];
let monthsBack = 0;
let timestamp;
do {
timestamp = moment().utc().subtract(monthsBack, 'months').startOf('month');
if (timestamp.valueOf() >= sliderMinValue) {
// Months are broken by the year tag, e.g. November, December, 2016, February, etc...
let label = timestamp.format('MMMM');
if (label === 'January') {
label = timestamp.format('YYYY');
}
ticks.push({ timestampValue: timestamp.valueOf(), label });
}
monthsBack += 1;
} while (timestamp.valueOf() >= sliderMinValue);
return (
<div className="time-travel-markers">
{map(ticks, tick => this.renderMark(tick))}
</div>
);
}
render() {
const { sliderValue, sliderMinValue, inputValue } = this.state;
const sliderMaxValue = moment().valueOf();
const className = classNames('time-travel', { visible: this.props.showingTimeTravel });
return (
<div className={className}>
<div className="time-travel-slider-wrapper">
{this.renderMarks()}
<Slider
onChange={this.handleSliderChange}
value={sliderValue}
min={sliderMinValue}
max={sliderMaxValue}
/>
</div>
<div className="time-travel-jump-controls">
<a className="button jump" onClick={() => this.handleJumpClick(-ONE_HOUR_MS)}>
<span className="fa fa-fast-backward" /> 1 hour
</a>
<a className="button jump" onClick={() => this.handleJumpClick(-FIVE_MINUTES_MS)}>
<span className="fa fa-step-backward" /> 5 mins
</a>
<span className="time-travel-jump-controls-timestamp">
<input value={inputValue} onChange={this.handleInputChange} /> UTC
</span>
<a className="button jump" onClick={() => this.handleJumpClick(+FIVE_MINUTES_MS)}>
<span className="fa fa-step-forward" /> 5 mins
</a>
<a className="button jump" onClick={() => this.handleJumpClick(+ONE_HOUR_MS)}>
<span className="fa fa-fast-forward" /> 1 hour
</a>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
showingTimeTravel: state.get('showingTimeTravel'),
topologyViewMode: state.get('topologyViewMode'),
currentTopology: state.get('currentTopology'),
pausedAt: state.get('pausedAt'),
};
}
export default connect(
mapStateToProps,
{
jumpToTime,
resumeTime,
timeTravelStartTransition,
}
)(TimeTravel);