Merge pull request #2903 from weaveworks/use-time-travel-from-ui-components-repo

Use TimeTravel from ui-components repo
This commit is contained in:
Filip Barl
2017-10-23 12:50:28 +02:00
committed by GitHub
13 changed files with 32 additions and 648 deletions

View File

@@ -4,8 +4,9 @@ import { Repeat, fromJS, Map as makeMap } from 'immutable';
import { line, curveBasis } from 'd3-shape';
import { times } from 'lodash';
import { weakSpring } from 'weaveworks-ui-components/lib/utils/animation';
import { NODE_BASE_SIZE, EDGE_WAYPOINTS_CAP } from '../constants/styles';
import { weakSpring } from '../utils/animation-utils';
import Edge from './edge';

View File

@@ -1,7 +1,8 @@
import React from 'react';
import { Motion } from 'react-motion';
import { weakSpring } from '../utils/animation-utils';
import { weakSpring } from 'weaveworks-ui-components/lib/utils/animation';
import Node from './node';

View File

@@ -35,8 +35,8 @@ import {
} from '../actions/app-actions';
import Details from './details';
import Nodes from './nodes';
import TimeTravel from './time-travel';
import TimeControl from './time-control';
import TimeTravelWrapper from './time-travel-wrapper';
import ViewModeSelector from './view-mode-selector';
import NetworkSelector from './networks-selector';
import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar';
@@ -85,7 +85,7 @@ class App extends React.Component {
}
componentWillUnmount() {
window.addEventListener('resize', this.handleResize);
window.removeEventListener('resize', this.handleResize);
window.removeEventListener('keypress', this.onKeyPress);
window.removeEventListener('keyup', this.onKeyUp);
this.props.dispatch(shutdown());
@@ -191,7 +191,7 @@ class App extends React.Component {
{showingDetails && <Details />}
<div className="header">
<TimeTravel />
<TimeTravelWrapper />
<div className="selectors">
<div className="logo">
{!isIframe &&

View File

@@ -1,578 +0,0 @@
import React from 'react';
import moment from 'moment';
import styled from 'styled-components';
import { map, clamp, find, last, debounce } from 'lodash';
import { drag } from 'd3-drag';
import { scaleUtc } from 'd3-scale';
import { event as d3Event, select } from 'd3-selection';
import { Motion } from 'react-motion';
import { zoomFactor } from '../utils/zoom-utils';
import { strongSpring } from '../utils/animation-utils';
import { linearGradientValue } from '../utils/math-utils';
import {
nowInSecondsPrecision,
clampToNowInSecondsPrecision,
scaleDuration,
} from '../utils/time-utils';
const TICK_SETTINGS_PER_PERIOD = {
year: {
format: 'YYYY',
childPeriod: 'month',
intervals: [
moment.duration(1, 'year'),
],
},
month: {
format: 'MMMM',
parentPeriod: 'year',
childPeriod: 'day',
intervals: [
moment.duration(1, 'month'),
moment.duration(3, 'months'),
],
},
day: {
format: 'Do',
parentPeriod: 'month',
childPeriod: 'minute',
intervals: [
moment.duration(1, 'day'),
moment.duration(1, 'week'),
],
},
minute: {
format: 'HH:mm',
parentPeriod: 'day',
intervals: [
moment.duration(1, 'minute'),
moment.duration(5, 'minutes'),
moment.duration(15, 'minutes'),
moment.duration(1, 'hour'),
moment.duration(3, 'hours'),
moment.duration(6, 'hours'),
],
},
};
const ZOOM_TRACK_DEBOUNCE_INTERVAL = 5000;
const TIMELINE_DEBOUNCE_INTERVAL = 500;
const TIMELINE_TICK_INTERVAL = 1000;
const TIMELINE_HEIGHT = '55px';
const MIN_DURATION_PER_PX = moment.duration(250, 'milliseconds');
const INIT_DURATION_PER_PX = moment.duration(1, 'minute');
const MAX_DURATION_PER_PX = moment.duration(3, 'days');
const MIN_TICK_SPACING_PX = 70;
const MAX_TICK_SPACING_PX = 415;
const FADE_OUT_FACTOR = 1.4;
const TICKS_ROW_SPACING = 16;
const MAX_TICK_ROWS = 3;
// From https://stackoverflow.com/a/18294634
const FullyPannableCanvas = styled.svg`
width: 100%;
height: 100%;
cursor: move;
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
${props => props.panning && `
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
`}
`;
const TimeTravelContainer = styled.div`
transition: all .15s ease-in-out;
position: relative;
margin-bottom: 15px;
overflow: hidden;
z-index: 2001;
height: 0;
${props => props.visible && `
height: calc(${TIMELINE_HEIGHT} + 35px);
margin-bottom: 15px;
margin-top: -5px;
`}
`;
const TimelineContainer = styled.div`
align-items: center;
display: flex;
height: ${TIMELINE_HEIGHT};
&:before, &:after {
border: 1px solid ${props => props.theme.colors.white};
background-color: ${props => props.theme.colors.accent.orange};
content: '';
position: absolute;
display: block;
left: 50%;
border-top: 0;
border-bottom: 0;
margin-left: -1px;
width: 3px;
}
&:before {
top: 0;
height: ${TIMELINE_HEIGHT};
}
&:after {
top: ${TIMELINE_HEIGHT};
height: 9px;
opacity: 0.15;
}
`;
const Timeline = FullyPannableCanvas.extend`
background-color: rgba(255, 255, 255, 0.85);
box-shadow: inset 0 0 7px ${props => props.theme.colors.gray};
pointer-events: all;
margin: 0 7px;
`;
const DisabledRange = styled.rect`
fill: ${props => props.theme.colors.gray};
fill-opacity: 0.15;
`;
const ShallowButton = styled.button`
background-color: transparent;
border: 0;
color: ${props => props.theme.colors.primary.lavender};
cursor: pointer;
padding: 0;
outline: 0;
&:hover {
color: ${props => props.theme.colors.primary.charcoal};
}
`;
const TimestampLabel = ShallowButton.extend`
margin-left: 2px;
padding: 3px;
&[disabled] {
color: ${props => props.theme.colors.gray};
cursor: inherit;
}
`;
const TimelinePanButton = ShallowButton.extend`
pointer-events: all;
padding: 2px;
`;
const TimestampContainer = styled.div`
background-color: ${props => props.theme.colors.white};
border: 1px solid ${props => props.theme.colors.gray};
border-radius: 4px;
padding: 2px 8px;
pointer-events: all;
margin: 8px 0 25px 50%;
transform: translateX(-50%);
opacity: 0.8;
display: inline-block;
`;
const TimestampInput = styled.input`
background-color: transparent;
font-family: "Roboto", sans-serif;
text-align: center;
font-size: 1rem;
width: 165px;
border: 0;
outline: 0;
`;
function getTimeScale({ focusedTimestamp, durationPerPixel }) {
const roundedTimestamp = moment(focusedTimestamp).utc().startOf('second');
const startDate = moment(roundedTimestamp).subtract(durationPerPixel);
const endDate = moment(roundedTimestamp).add(durationPerPixel);
return scaleUtc()
.domain([startDate, endDate])
.range([-1, 1]);
}
function findOptimalDurationFit(durations, { durationPerPixel }) {
const minimalDuration = scaleDuration(durationPerPixel, 1.1 * MIN_TICK_SPACING_PX);
return find(durations, d => d >= minimalDuration);
}
function getInputValue(timestamp) {
return {
inputValue: (timestamp ? moment(timestamp) : moment()).utc().format(),
};
}
export default class TimeTravelComponent extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
timestampNow: nowInSecondsPrecision(),
focusedTimestamp: nowInSecondsPrecision(),
durationPerPixel: INIT_DURATION_PER_PX,
boundingRect: { width: 0, height: 0 },
isPanning: false,
...getInputValue(props.timestamp),
};
this.jumpRelativePixels = this.jumpRelativePixels.bind(this);
this.jumpForward = this.jumpForward.bind(this);
this.jumpBackward = this.jumpBackward.bind(this);
this.jumpTo = this.jumpTo.bind(this);
this.handleZoom = this.handleZoom.bind(this);
this.handlePanStart = this.handlePanStart.bind(this);
this.handlePanEnd = this.handlePanEnd.bind(this);
this.handlePan = this.handlePan.bind(this);
this.saveSvgRef = this.saveSvgRef.bind(this);
this.debouncedTrackZoom = debounce(this.trackZoom.bind(this), ZOOM_TRACK_DEBOUNCE_INTERVAL);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleTimelinePan = this.handleTimelinePan.bind(this);
this.handleTimelinePanEnd = this.handleTimelinePanEnd.bind(this);
this.handleInstantJump = this.handleInstantJump.bind(this);
this.instantUpdateTimestamp = this.instantUpdateTimestamp.bind(this);
this.debouncedUpdateTimestamp = debounce(
this.instantUpdateTimestamp.bind(this),
TIMELINE_DEBOUNCE_INTERVAL
);
}
componentDidMount() {
this.svg = select('.time-travel-timeline svg');
this.drag = drag()
.on('start', this.handlePanStart)
.on('end', this.handlePanEnd)
.on('drag', this.handlePan);
this.svg.call(this.drag);
// Force periodic updates of the availability range as time goes by.
this.timer = setInterval(() => {
this.setState({ timestampNow: nowInSecondsPrecision() });
}, TIMELINE_TICK_INTERVAL);
}
componentWillUnmount() {
clearInterval(this.timer);
}
componentWillReceiveProps(nextProps) {
// Update the input value
this.setState(getInputValue(nextProps.timestamp));
// Don't update the focused timestamp if we're not paused (so the timeline is hidden).
if (nextProps.timestamp) {
this.setState({ focusedTimestamp: nextProps.timestamp });
}
// Always update the timeline dimension information.
this.setState({ boundingRect: this.svgRef.getBoundingClientRect() });
}
handleInputChange(ev) {
const timestamp = moment(ev.target.value);
this.setState({ inputValue: ev.target.value });
if (timestamp.isValid()) {
const clampedTimestamp = clampToNowInSecondsPrecision(timestamp);
this.instantUpdateTimestamp(clampedTimestamp, this.props.trackTimestampEdit);
}
}
handleTimelinePan(timestamp) {
this.setState(getInputValue(timestamp));
this.debouncedUpdateTimestamp(timestamp);
}
handleTimelinePanEnd(timestamp) {
this.instantUpdateTimestamp(timestamp, this.props.trackTimelinePan);
}
handleInstantJump(timestamp) {
this.instantUpdateTimestamp(timestamp, this.props.trackTimelineClick);
}
handlePanStart() {
this.setState({ isPanning: true });
}
handlePanEnd() {
this.handleTimelinePanEnd(this.state.focusedTimestamp);
this.setState({ isPanning: false });
}
handlePan() {
const dragDuration = scaleDuration(this.state.durationPerPixel, -d3Event.dx);
const timestamp = moment(this.state.focusedTimestamp).add(dragDuration);
const focusedTimestamp = clampToNowInSecondsPrecision(timestamp);
this.handleTimelinePan(focusedTimestamp);
this.setState({ focusedTimestamp });
}
handleZoom(ev) {
let durationPerPixel = scaleDuration(this.state.durationPerPixel, 1 / zoomFactor(ev));
if (durationPerPixel > MAX_DURATION_PER_PX) durationPerPixel = MAX_DURATION_PER_PX;
if (durationPerPixel < MIN_DURATION_PER_PX) durationPerPixel = MIN_DURATION_PER_PX;
this.setState({ durationPerPixel });
this.debouncedTrackZoom();
ev.preventDefault();
}
instantUpdateTimestamp(timestamp, callback) {
if (!timestamp.isSame(this.props.timestamp)) {
this.debouncedUpdateTimestamp.cancel();
this.setState(getInputValue(timestamp));
this.props.changeTimestamp(moment(timestamp));
// Used for tracking.
if (callback) callback();
}
}
saveSvgRef(ref) {
this.svgRef = ref;
}
trackZoom() {
const periods = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'];
const duration = scaleDuration(this.state.durationPerPixel, MAX_TICK_SPACING_PX);
const zoomedPeriod = find(periods, period => Math.floor(duration.get(period)) && period);
this.props.trackTimelineZoom(zoomedPeriod);
}
jumpTo(timestamp) {
const focusedTimestamp = clampToNowInSecondsPrecision(timestamp);
this.handleInstantJump(focusedTimestamp);
this.setState({ focusedTimestamp });
}
jumpRelativePixels(pixels) {
const duration = scaleDuration(this.state.durationPerPixel, pixels);
const timestamp = moment(this.state.focusedTimestamp).add(duration);
this.jumpTo(timestamp);
}
jumpForward() {
this.jumpRelativePixels(this.state.boundingRect.width / 4);
}
jumpBackward() {
this.jumpRelativePixels(-this.state.boundingRect.width / 4);
}
getVerticalShiftForPeriod(period, { durationPerPixel }) {
const { childPeriod, parentPeriod } = TICK_SETTINGS_PER_PERIOD[period];
let shift = 1;
if (parentPeriod) {
const durationMultiplier = 1 / MAX_TICK_SPACING_PX;
const parentPeriodStartInterval = TICK_SETTINGS_PER_PERIOD[parentPeriod].intervals[0];
const fadedInDuration = scaleDuration(parentPeriodStartInterval, durationMultiplier);
const fadedOutDuration = scaleDuration(fadedInDuration, FADE_OUT_FACTOR);
const durationLog = d => Math.log(d.asMilliseconds());
const transitionFactor = durationLog(fadedOutDuration) - durationLog(durationPerPixel);
const transitionLength = durationLog(fadedOutDuration) - durationLog(fadedInDuration);
shift = clamp(transitionFactor / transitionLength, 0, 1);
}
if (childPeriod) {
shift += this.getVerticalShiftForPeriod(childPeriod, { durationPerPixel });
}
return shift;
}
getTicksForPeriod(period, timelineTransform) {
// First find the optimal duration between the ticks - if no satisfactory
// duration could be found, don't render any ticks for the given period.
const { parentPeriod, intervals } = TICK_SETTINGS_PER_PERIOD[period];
const duration = findOptimalDurationFit(intervals, timelineTransform);
if (!duration) return [];
// Get the boundary values for the displayed part of the timeline.
const timeScale = getTimeScale(timelineTransform);
const startPosition = -this.state.boundingRect.width / 2;
const endPosition = this.state.boundingRect.width / 2;
const startDate = moment(timeScale.invert(startPosition));
const endDate = moment(timeScale.invert(endPosition));
// Start counting the timestamps from the most recent timestamp that is not shown
// on screen. The values are always rounded up to the timestamps of the next bigger
// period (e.g. for days it would be months, for months it would be years).
let timestamp = moment(startDate).utc().startOf(parentPeriod || period);
while (timestamp.isBefore(startDate)) {
timestamp = moment(timestamp).add(duration);
}
timestamp = moment(timestamp).subtract(duration);
// Make that hidden timestamp the first one in the list, but position
// it inside the visible range with a prepended arrow to the past.
const ticks = [{
timestamp: moment(timestamp),
position: startPosition,
isBehind: true,
}];
// Continue adding ticks till the end of the visible range.
do {
// If the new timestamp enters into a new bigger period, we round it down to the
// beginning of that period. E.g. instead of going [Jan 22nd, Jan 29th, Feb 5th],
// we output [Jan 22nd, Jan 29th, Feb 1st]. Right now this case only happens between
// days and months, but in theory it could happen whenever bigger periods are not
// divisible by the duration we are using as a step between the ticks.
let newTimestamp = moment(timestamp).add(duration);
if (parentPeriod && newTimestamp.get(parentPeriod) !== timestamp.get(parentPeriod)) {
newTimestamp = moment(newTimestamp).utc().startOf(parentPeriod);
}
timestamp = newTimestamp;
// If the new tick is too close to the previous one, drop that previous tick.
const position = timeScale(timestamp);
const previousPosition = last(ticks) && last(ticks).position;
if (position - previousPosition < MIN_TICK_SPACING_PX) {
ticks.pop();
}
ticks.push({ timestamp, position });
} while (timestamp.isBefore(endDate));
return ticks;
}
renderTimestampTick({ timestamp, position, isBehind }, periodFormat, opacity) {
// Ticks are disabled if they are in the future or if they are too transparent.
const disabled = timestamp.isAfter(this.state.timestampNow) || opacity < 0.4;
const handleClick = () => this.jumpTo(timestamp);
return (
<g transform={`translate(${position}, 0)`} key={timestamp.format()}>
{!isBehind && <line y2="75" stroke="#ddd" strokeWidth="1" />}
{!disabled && <title>Jump to {timestamp.utc().format()}</title>}
<foreignObject width="100" height="20" style={{ lineHeight: '20px' }}>
<TimestampLabel disabled={disabled} onClick={!disabled && handleClick}>
{timestamp.utc().format(periodFormat)}
</TimestampLabel>
</foreignObject>
</g>
);
}
renderPeriodTicks(period, timelineTransform) {
const periodFormat = TICK_SETTINGS_PER_PERIOD[period].format;
const ticks = this.getTicksForPeriod(period, timelineTransform);
const ticksRow = MAX_TICK_ROWS - this.getVerticalShiftForPeriod(period, timelineTransform);
const transform = `translate(0, ${ticksRow * TICKS_ROW_SPACING})`;
// Ticks quickly fade in from the bottom and then slowly start
// fading out towards the top until they are pushed out of canvas.
const focusedRow = MAX_TICK_ROWS - 1;
const opacity = ticksRow > focusedRow ?
linearGradientValue(ticksRow, [MAX_TICK_ROWS, focusedRow]) :
linearGradientValue(ticksRow, [-2, focusedRow]);
return (
<g className={period} transform={transform} style={{ opacity }}>
{map(ticks, tick => this.renderTimestampTick(tick, periodFormat, opacity))}
</g>
);
}
renderDisabledShadow(timelineTransform) {
const timeScale = getTimeScale(timelineTransform);
const nowShift = timeScale(this.state.timestampNow);
const { width, height } = this.state.boundingRect;
return (
<DisabledRange transform={`translate(${nowShift}, 0)`} width={width} height={height} />
);
}
renderAxis(timelineTransform) {
const { width, height } = this.state.boundingRect;
return (
<g id="axis">
<rect
className="tooltip-container"
transform={`translate(${-width / 2}, 0)`}
width={width}
height={height}
fillOpacity={0}
/>
{this.renderDisabledShadow(timelineTransform)}
<g className="ticks" transform="translate(0, 1)">
{this.renderPeriodTicks('year', timelineTransform)}
{this.renderPeriodTicks('month', timelineTransform)}
{this.renderPeriodTicks('day', timelineTransform)}
{this.renderPeriodTicks('minute', timelineTransform)}
</g>
</g>
);
}
renderAnimatedContent() {
const focusedTimestampValue = this.state.focusedTimestamp.valueOf();
const durationPerPixelValue = this.state.durationPerPixel.asMilliseconds();
return (
<Motion
style={{
focusedTimestampValue: strongSpring(focusedTimestampValue),
durationPerPixelValue: strongSpring(durationPerPixelValue),
}}>
{interpolated => this.renderAxis({
focusedTimestamp: moment(interpolated.focusedTimestampValue),
durationPerPixel: moment.duration(interpolated.durationPerPixelValue),
})}
</Motion>
);
}
render() {
const { isPanning, boundingRect } = this.state;
const halfWidth = boundingRect.width / 2;
return (
<TimeTravelContainer visible={this.props.visible}>
<TimelineContainer className="time-travel-timeline">
<TimelinePanButton onClick={this.jumpBackward}>
<span className="fa fa-chevron-left" />
</TimelinePanButton>
<Timeline panning={isPanning} innerRef={this.saveSvgRef} onWheel={this.handleZoom}>
<g className="timeline-container" transform={`translate(${halfWidth}, 0)`}>
<title>Scroll to zoom, drag to pan</title>
{this.renderAnimatedContent()}
</g>
</Timeline>
<TimelinePanButton onClick={this.jumpForward}>
<span className="fa fa-chevron-right" />
</TimelinePanButton>
</TimelineContainer>
<TimestampContainer>
<TimestampInput
value={this.state.inputValue}
onChange={this.handleInputChange}
/> UTC
</TimestampContainer>
</TimeTravelContainer>
);
}
}

View File

@@ -1,12 +1,13 @@
import React from 'react';
import moment from 'moment';
import { connect } from 'react-redux';
import { TimeTravel } from 'weaveworks-ui-components';
import TimeTravelComponent from './time-travel-component';
import { trackAnalyticsEvent } from '../utils/tracking-utils';
import { jumpToTime } from '../actions/app-actions';
class TimeTravel extends React.Component {
class TimeTravelWrapper extends React.Component {
constructor(props, context) {
super(props, context);
@@ -55,18 +56,17 @@ class TimeTravel extends React.Component {
}
render() {
const { visible, timestamp, viewportWidth } = this.props;
const { visible, timestamp } = this.props;
return (
<TimeTravelComponent
<TimeTravel
visible={visible}
timestamp={timestamp}
viewportWidth={viewportWidth}
changeTimestamp={this.changeTimestamp}
trackTimestampEdit={this.trackTimestampEdit}
trackTimelineClick={this.trackTimelineClick}
trackTimelineZoom={this.trackTimelineZoom}
trackTimelinePan={this.trackTimelinePan}
timestamp={timestamp || moment()}
onChange={this.changeTimestamp}
onTimestampInputEdit={this.trackTimestampEdit}
onTimestampLabelClick={this.trackTimelineClick}
onTimelineZoom={this.trackTimelineZoom}
onTimelinePan={this.trackTimelinePan}
/>
);
}
@@ -78,12 +78,10 @@ function mapStateToProps(state) {
topologyViewMode: state.get('topologyViewMode'),
currentTopology: state.get('currentTopology'),
timestamp: state.get('pausedAt'),
// Used only to trigger recalculations on window resize.
viewportWidth: state.getIn(['viewport', 'width']),
};
}
export default connect(
mapStateToProps,
{ jumpToTime },
)(TimeTravel);
)(TimeTravelWrapper);

View File

@@ -6,11 +6,11 @@ import { fromJS } from 'immutable';
import { drag } from 'd3-drag';
import { event as d3Event, select } from 'd3-selection';
import { zoomFactor } from 'weaveworks-ui-components/lib/utils/zooming';
import Logo from '../components/logo';
import ZoomControl from '../components/zoom-control';
import { cacheZoomState } from '../actions/app-actions';
import { zoomFactor } from '../utils/zoom-utils';
import { applyTransform, inverseTransform } from '../utils/transform-utils';
import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming';
import {

View File

@@ -9,6 +9,8 @@ import {
OrderedMap as makeOrderedMap,
} from 'immutable';
import { nowInSecondsPrecision } from 'weaveworks-ui-components/lib/utils/time';
import ActionTypes from '../constants/action-types';
import {
GRAPH_VIEW_MODE,
@@ -20,7 +22,7 @@ import {
} from '../selectors/topology';
import { isPausedSelector } from '../selectors/time-travel';
import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming';
import { nowInSecondsPrecision, timestampsEqual } from '../utils/time-utils';
import { timestampsEqual } from '../utils/time-utils';
import { applyPinnedSearches } from '../utils/search-utils';
import {
findTopologyById,

View File

@@ -1,10 +0,0 @@
import { spring } from 'react-motion';
export function weakSpring(value) {
return spring(value, { stiffness: 100, damping: 18, precision: 1 });
}
export function strongSpring(value) {
return spring(value, { stiffness: 800, damping: 50, precision: 1 });
}

View File

@@ -39,8 +39,3 @@ export function minEuclideanDistanceBetweenPoints(points) {
});
return minDistance;
}
// A linear mapping [a, b] -> [0, 1] (maps value x=a into 0 and x=b into 1).
export function linearGradientValue(x, [a, b]) {
return (x - a) / (b - a);
}

View File

@@ -1,4 +1,3 @@
import moment from 'moment';
// Replacement for timely dependency
export function timer(fn) {
@@ -11,20 +10,6 @@ export function timer(fn) {
return timedFn;
}
export function nowInSecondsPrecision() {
return moment().startOf('second');
}
export function clampToNowInSecondsPrecision(timestamp) {
const now = nowInSecondsPrecision();
return timestamp.isAfter(now) ? now : timestamp;
}
// This is unfortunately not there in moment.js
export function scaleDuration(duration, scale) {
return moment.duration(duration.asMilliseconds() * scale);
}
export function timestampsEqual(timestampA, timestampB) {
const stringifiedTimestampA = timestampA ? timestampA.toISOString() : '';
const stringifiedTimestampB = timestampB ? timestampB.toISOString() : '';

View File

@@ -1,15 +0,0 @@
const ZOOM_SENSITIVITY = 0.0025;
const DOM_DELTA_LINE = 1;
// See https://github.com/d3/d3-zoom/blob/807f02c7a5fe496fbd08cc3417b62905a8ce95fa/src/zoom.js
function wheelDelta(ev) {
// Only Firefox seems to use the line unit (which we assume to
// be 25px), otherwise the delta is already measured in pixels.
const unitInPixels = (ev.deltaMode === DOM_DELTA_LINE ? 25 : 1);
return -ev.deltaY * unitInPixels * ZOOM_SENSITIVITY;
}
export function zoomFactor(ev) {
return Math.exp(wheelDelta(ev));
}

View File

@@ -44,7 +44,7 @@
"reselect": "3.0.1",
"reselect-map": "1.0.3",
"styled-components": "^2.2.1",
"weaveworks-ui-components": "git+https://github.com/weaveworks/ui-components.git#v0.1.45",
"weaveworks-ui-components": "git+https://github.com/weaveworks/ui-components.git#v0.1.47",
"whatwg-fetch": "2.0.3",
"xterm": "2.9.2"
},

View File

@@ -6666,9 +6666,9 @@ wd@^0.4.0:
underscore.string "~3.0.3"
vargs "~0.1.0"
"weaveworks-ui-components@git+https://github.com/weaveworks/ui-components.git#v0.1.45":
version "0.1.45"
resolved "git+https://github.com/weaveworks/ui-components.git#9b700e6231599d0f7b353d8e2bb5f466394dc081"
"weaveworks-ui-components@git+https://github.com/weaveworks/ui-components.git#v0.1.47":
version "0.1.47"
resolved "git+https://github.com/weaveworks/ui-components.git#1a112eef36f282d754502f77ab798381f5d3a995"
dependencies:
babel-cli "^6.18.0"
babel-plugin-transform-export-extensions "6.8.0"
@@ -6676,9 +6676,14 @@ wd@^0.4.0:
babel-preset-es2015 "6.18.0"
babel-preset-react "6.16.0"
classnames "^2.2.5"
d3-drag "1.2.1"
d3-scale "1.0.6"
d3-selection "1.1.0"
moment "2.19.1"
node-sass "4.5.3"
polished "^1.7.0"
prop-types "^15.5.8"
react-motion "0.5.2"
webidl-conversions@^3.0.0:
version "3.0.1"