From ada122c1923b79eec86078a99753644393543552 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 11 Oct 2017 22:31:09 +0200 Subject: [PATCH 1/7] Started making component more independent. --- .../components/time-travel-component.js | 144 ++++++++++++++++++ client/app/scripts/components/time-travel.js | 139 ++--------------- 2 files changed, 156 insertions(+), 127 deletions(-) create mode 100644 client/app/scripts/components/time-travel-component.js diff --git a/client/app/scripts/components/time-travel-component.js b/client/app/scripts/components/time-travel-component.js new file mode 100644 index 000000000..a962eb7b0 --- /dev/null +++ b/client/app/scripts/components/time-travel-component.js @@ -0,0 +1,144 @@ +import React from 'react'; +import moment from 'moment'; +import classNames from 'classnames'; +import { connect } from 'react-redux'; +import { debounce } from 'lodash'; + +import TimeTravelTimeline from './time-travel-timeline'; +import { trackAnalyticsEvent } from '../utils/tracking-utils'; +import { clampToNowInSecondsPrecision } from '../utils/time-utils'; +import { + jumpToTime, + resumeTime, + timeTravelStartTransition, +} from '../actions/app-actions'; + +import { TIMELINE_DEBOUNCE_INTERVAL } from '../constants/timer'; + + +const getTimestampStates = (timestamp) => { + timestamp = timestamp || moment(); + return { + inputValue: moment(timestamp).utc().format(), + }; +}; + +class TimeTravelComponent extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = getTimestampStates(props.timestamp); + + 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.trackTimestampEdit = this.trackTimestampEdit.bind(this); + this.trackTimelineClick = this.trackTimelineClick.bind(this); + this.trackTimelinePan = this.trackTimelinePan.bind(this); + + this.instantUpdateTimestamp = this.instantUpdateTimestamp.bind(this); + this.debouncedUpdateTimestamp = debounce( + this.instantUpdateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL); + } + + componentWillReceiveProps(props) { + this.setState(getTimestampStates(props.timestamp)); + } + + 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.trackTimestampEdit); + } + } + + handleTimelinePan(timestamp) { + this.setState(getTimestampStates(timestamp)); + this.debouncedUpdateTimestamp(timestamp); + } + + handleTimelinePanEnd(timestamp) { + this.instantUpdateTimestamp(timestamp, this.trackTimelinePan); + } + + handleInstantJump(timestamp) { + this.instantUpdateTimestamp(timestamp, this.trackTimelineClick); + } + + instantUpdateTimestamp(timestamp, callback) { + if (!timestamp.isSame(this.props.timestamp)) { + this.debouncedUpdateTimestamp.cancel(); + this.setState(getTimestampStates(timestamp)); + this.props.timeTravelStartTransition(); + this.props.jumpToTime(moment(timestamp)); + + // Used for tracking. + if (callback) callback(); + } + } + + trackTimestampEdit() { + trackAnalyticsEvent('scope.time.timestamp.edit', { + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + } + + trackTimelineClick() { + trackAnalyticsEvent('scope.time.timeline.click', { + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + } + + trackTimelinePan() { + trackAnalyticsEvent('scope.time.timeline.pan', { + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + } + + render() { + const { visible } = this.props; + + return ( +
+ +
+ UTC +
+
+ ); + } +} + +function mapStateToProps(state) { + return { + topologyViewMode: state.get('topologyViewMode'), + currentTopology: state.get('currentTopology'), + }; +} + +export default connect( + mapStateToProps, + { + jumpToTime, + resumeTime, + timeTravelStartTransition, + } +)(TimeTravelComponent); diff --git a/client/app/scripts/components/time-travel.js b/client/app/scripts/components/time-travel.js index 79528bb2a..672747c5b 100644 --- a/client/app/scripts/components/time-travel.js +++ b/client/app/scripts/components/time-travel.js @@ -1,144 +1,29 @@ import React from 'react'; -import moment from 'moment'; -import classNames from 'classnames'; import { connect } from 'react-redux'; -import { debounce } from 'lodash'; -import TimeTravelTimeline from './time-travel-timeline'; -import { trackAnalyticsEvent } from '../utils/tracking-utils'; -import { clampToNowInSecondsPrecision } from '../utils/time-utils'; -import { - jumpToTime, - resumeTime, - timeTravelStartTransition, -} from '../actions/app-actions'; +import TimeTravelComponent from './time-travel-component'; -import { TIMELINE_DEBOUNCE_INTERVAL } from '../constants/timer'; - - -const getTimestampStates = (timestamp) => { - timestamp = timestamp || moment(); - return { - inputValue: moment(timestamp).utc().format(), - }; -}; class TimeTravel extends React.Component { - constructor(props, context) { - super(props, context); - - this.state = getTimestampStates(props.pausedAt); - - 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.trackTimestampEdit = this.trackTimestampEdit.bind(this); - this.trackTimelineClick = this.trackTimelineClick.bind(this); - this.trackTimelinePan = this.trackTimelinePan.bind(this); - - this.instantUpdateTimestamp = this.instantUpdateTimestamp.bind(this); - this.debouncedUpdateTimestamp = debounce( - this.instantUpdateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL); - } - - componentWillReceiveProps(props) { - this.setState(getTimestampStates(props.pausedAt)); - } - - 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.trackTimestampEdit); - } - } - - handleTimelinePan(timestamp) { - this.setState(getTimestampStates(timestamp)); - this.debouncedUpdateTimestamp(timestamp); - } - - handleTimelinePanEnd(timestamp) { - this.instantUpdateTimestamp(timestamp, this.trackTimelinePan); - } - - handleInstantJump(timestamp) { - this.instantUpdateTimestamp(timestamp, this.trackTimelineClick); - } - - instantUpdateTimestamp(timestamp, callback) { - if (!timestamp.isSame(this.props.pausedAt)) { - this.debouncedUpdateTimestamp.cancel(); - this.setState(getTimestampStates(timestamp)); - this.props.timeTravelStartTransition(); - this.props.jumpToTime(moment(timestamp)); - - // Used for tracking. - if (callback) callback(); - } - } - - trackTimestampEdit() { - trackAnalyticsEvent('scope.time.timestamp.edit', { - layout: this.props.topologyViewMode, - topologyId: this.props.currentTopology.get('id'), - parentTopologyId: this.props.currentTopology.get('parentId'), - }); - } - - trackTimelineClick() { - trackAnalyticsEvent('scope.time.timeline.click', { - layout: this.props.topologyViewMode, - topologyId: this.props.currentTopology.get('id'), - parentTopologyId: this.props.currentTopology.get('parentId'), - }); - } - - trackTimelinePan() { - trackAnalyticsEvent('scope.time.timeline.pan', { - layout: this.props.topologyViewMode, - topologyId: this.props.currentTopology.get('id'), - parentTopologyId: this.props.currentTopology.get('parentId'), - }); - } - render() { + const { visible, timestamp } = this.props; + return ( -
- -
- UTC -
-
+ ); } } function mapStateToProps(state) { return { - showingTimeTravel: state.get('showingTimeTravel'), - topologyViewMode: state.get('topologyViewMode'), - currentTopology: state.get('currentTopology'), - pausedAt: state.get('pausedAt'), + visible: state.get('showingTimeTravel'), + // topologyViewMode: state.get('topologyViewMode'), + // currentTopology: state.get('currentTopology'), + timestamp: state.get('pausedAt'), }; } -export default connect( - mapStateToProps, - { - jumpToTime, - resumeTime, - timeTravelStartTransition, - } -)(TimeTravel); +export default connect(mapStateToProps)(TimeTravel); From 742a8c528ca1d19c4784854d52981ad043b2de16 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 11 Oct 2017 22:48:20 +0200 Subject: [PATCH 2/7] Made two time travel component parts independent of global state. --- .../components/time-travel-component.js | 66 +++---------------- .../components/time-travel-timeline.js | 18 +---- client/app/scripts/components/time-travel.js | 62 +++++++++++++++-- 3 files changed, 68 insertions(+), 78 deletions(-) diff --git a/client/app/scripts/components/time-travel-component.js b/client/app/scripts/components/time-travel-component.js index a962eb7b0..3281ece00 100644 --- a/client/app/scripts/components/time-travel-component.js +++ b/client/app/scripts/components/time-travel-component.js @@ -1,17 +1,10 @@ import React from 'react'; import moment from 'moment'; import classNames from 'classnames'; -import { connect } from 'react-redux'; import { debounce } from 'lodash'; import TimeTravelTimeline from './time-travel-timeline'; -import { trackAnalyticsEvent } from '../utils/tracking-utils'; import { clampToNowInSecondsPrecision } from '../utils/time-utils'; -import { - jumpToTime, - resumeTime, - timeTravelStartTransition, -} from '../actions/app-actions'; import { TIMELINE_DEBOUNCE_INTERVAL } from '../constants/timer'; @@ -23,7 +16,7 @@ const getTimestampStates = (timestamp) => { }; }; -class TimeTravelComponent extends React.Component { +export default class TimeTravelComponent extends React.Component { constructor(props, context) { super(props, context); @@ -34,10 +27,6 @@ class TimeTravelComponent extends React.Component { this.handleTimelinePanEnd = this.handleTimelinePanEnd.bind(this); this.handleInstantJump = this.handleInstantJump.bind(this); - this.trackTimestampEdit = this.trackTimestampEdit.bind(this); - this.trackTimelineClick = this.trackTimelineClick.bind(this); - this.trackTimelinePan = this.trackTimelinePan.bind(this); - this.instantUpdateTimestamp = this.instantUpdateTimestamp.bind(this); this.debouncedUpdateTimestamp = debounce( this.instantUpdateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL); @@ -53,7 +42,7 @@ class TimeTravelComponent extends React.Component { if (timestamp.isValid()) { const clampedTimestamp = clampToNowInSecondsPrecision(timestamp); - this.instantUpdateTimestamp(clampedTimestamp, this.trackTimestampEdit); + this.instantUpdateTimestamp(clampedTimestamp, this.props.trackTimestampEdit); } } @@ -63,55 +52,32 @@ class TimeTravelComponent extends React.Component { } handleTimelinePanEnd(timestamp) { - this.instantUpdateTimestamp(timestamp, this.trackTimelinePan); + this.instantUpdateTimestamp(timestamp, this.props.trackTimelinePan); } handleInstantJump(timestamp) { - this.instantUpdateTimestamp(timestamp, this.trackTimelineClick); + this.instantUpdateTimestamp(timestamp, this.props.trackTimelineClick); } instantUpdateTimestamp(timestamp, callback) { if (!timestamp.isSame(this.props.timestamp)) { this.debouncedUpdateTimestamp.cancel(); this.setState(getTimestampStates(timestamp)); - this.props.timeTravelStartTransition(); - this.props.jumpToTime(moment(timestamp)); + this.props.changeTimestamp(moment(timestamp)); // Used for tracking. if (callback) callback(); } } - trackTimestampEdit() { - trackAnalyticsEvent('scope.time.timestamp.edit', { - layout: this.props.topologyViewMode, - topologyId: this.props.currentTopology.get('id'), - parentTopologyId: this.props.currentTopology.get('parentId'), - }); - } - - trackTimelineClick() { - trackAnalyticsEvent('scope.time.timeline.click', { - layout: this.props.topologyViewMode, - topologyId: this.props.currentTopology.get('id'), - parentTopologyId: this.props.currentTopology.get('parentId'), - }); - } - - trackTimelinePan() { - trackAnalyticsEvent('scope.time.timeline.pan', { - layout: this.props.topologyViewMode, - topologyId: this.props.currentTopology.get('id'), - parentTopologyId: this.props.currentTopology.get('parentId'), - }); - } - render() { - const { visible } = this.props; + const { visible, timestamp, viewportWidth } = this.props; return (
); } @@ -20,10 +67,15 @@ class TimeTravel extends React.Component { function mapStateToProps(state) { return { visible: state.get('showingTimeTravel'), - // topologyViewMode: state.get('topologyViewMode'), - // currentTopology: state.get('currentTopology'), + 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)(TimeTravel); +export default connect( + mapStateToProps, + { jumpToTime, timeTravelStartTransition }, +)(TimeTravel); From 6b8424305c268e04f5d95327848f587933963a68 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 16 Oct 2017 15:15:30 +0200 Subject: [PATCH 3/7] Merged TimeTravelTimeline into TimeTravelComponent so that whole of time travel is contained in one component. --- client/app/scripts/actions/app-actions.js | 6 - .../components/time-travel-component.js | 409 +++++++++++++++++- .../components/time-travel-timeline.js | 393 ----------------- client/app/scripts/components/time-travel.js | 20 +- client/app/scripts/constants/action-types.js | 1 - client/app/scripts/reducers/root.js | 5 +- 6 files changed, 401 insertions(+), 433 deletions(-) delete mode 100644 client/app/scripts/components/time-travel-timeline.js diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index a16b5b9d3..a1b7ac27c 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -615,12 +615,6 @@ export function receiveNodes(nodes) { }; } -export function timeTravelStartTransition() { - return { - type: ActionTypes.TIME_TRAVEL_START_TRANSITION, - }; -} - export function jumpToTime(timestamp) { return (dispatch, getState) => { dispatch({ diff --git a/client/app/scripts/components/time-travel-component.js b/client/app/scripts/components/time-travel-component.js index 3281ece00..e270bc095 100644 --- a/client/app/scripts/components/time-travel-component.js +++ b/client/app/scripts/components/time-travel-component.js @@ -1,26 +1,124 @@ import React from 'react'; import moment from 'moment'; import classNames from 'classnames'; -import { debounce } from 'lodash'; +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 TimeTravelTimeline from './time-travel-timeline'; -import { clampToNowInSecondsPrecision } from '../utils/time-utils'; - -import { TIMELINE_DEBOUNCE_INTERVAL } from '../constants/timer'; +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 getTimestampStates = (timestamp) => { - timestamp = timestamp || moment(); - return { - inputValue: moment(timestamp).utc().format(), - }; +import { + TIMELINE_DEBOUNCE_INTERVAL, + TIMELINE_TICK_INTERVAL, + ZOOM_TRACK_DEBOUNCE_INTERVAL, +} from '../constants/timer'; + + +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 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; + + +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 = getTimestampStates(props.timestamp); + 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); @@ -32,8 +130,33 @@ export default class TimeTravelComponent extends React.Component { this.instantUpdateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL); } - componentWillReceiveProps(props) { - this.setState(getTimestampStates(props.timestamp)); + 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) { @@ -47,7 +170,7 @@ export default class TimeTravelComponent extends React.Component { } handleTimelinePan(timestamp) { - this.setState(getTimestampStates(timestamp)); + this.setState(getInputValue(timestamp)); this.debouncedUpdateTimestamp(timestamp); } @@ -59,10 +182,37 @@ export default class TimeTravelComponent extends React.Component { 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(getTimestampStates(timestamp)); + this.setState(getInputValue(timestamp)); this.props.changeTimestamp(moment(timestamp)); // Used for tracking. @@ -70,18 +220,231 @@ export default class TimeTravelComponent extends React.Component { } } + 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 ( + + {!isBehind && } + {!disabled && Jump to {timestamp.utc().format()}} + + + {timestamp.utc().format(periodFormat)} + + + + ); + } + + 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 ( + + {map(ticks, tick => this.renderTimestampTick(tick, periodFormat, opacity))} + + ); + } + + renderDisabledShadow(timelineTransform) { + const timeScale = getTimeScale(timelineTransform); + const nowShift = timeScale(this.state.timestampNow); + const { width, height } = this.state.boundingRect; + + return ( + + ); + } + + renderAxis(timelineTransform) { + const { width, height } = this.state.boundingRect; + + return ( + + + {this.renderDisabledShadow(timelineTransform)} + + {this.renderPeriodTicks('year', timelineTransform)} + {this.renderPeriodTicks('month', timelineTransform)} + {this.renderPeriodTicks('day', timelineTransform)} + {this.renderPeriodTicks('minute', timelineTransform)} + + + ); + } + + renderAnimatedContent() { + const focusedTimestampValue = this.state.focusedTimestamp.valueOf(); + const durationPerPixelValue = this.state.durationPerPixel.asMilliseconds(); + + return ( + + {interpolated => this.renderAxis({ + focusedTimestamp: moment(interpolated.focusedTimestampValue), + durationPerPixel: moment.duration(interpolated.durationPerPixelValue), + })} + + ); + } + render() { - const { visible, timestamp, viewportWidth } = this.props; + const { visible } = this.props; + const className = classNames({ panning: this.state.isPanning }); + const halfWidth = this.state.boundingRect.width / 2; return (
- +
+ + + + + + Scroll to zoom, drag to pan + {this.renderAnimatedContent()} + + + + + +
d >= minimalDuration); -} - - -export default class TimeTravelTimeline 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, - }; - - 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); - } - - 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) { - // 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() }); - } - - 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); - trackAnalyticsEvent('scope.time.timeline.zoom', { zoomedPeriod }); - } - - handlePanStart() { - this.setState({ isPanning: true }); - } - - handlePanEnd() { - this.props.onTimelinePanEnd(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.props.onTimelinePan(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(); - } - - jumpTo(timestamp) { - const focusedTimestamp = clampToNowInSecondsPrecision(timestamp); - this.props.onInstantJump(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 ( - - {!isBehind && } - {!disabled && Jump to {timestamp.utc().format()}} - - - {timestamp.utc().format(periodFormat)} - - - - ); - } - - 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 ( - - {map(ticks, tick => this.renderTimestampTick(tick, periodFormat, opacity))} - - ); - } - - renderDisabledShadow(timelineTransform) { - const timeScale = getTimeScale(timelineTransform); - const nowShift = timeScale(this.state.timestampNow); - const { width, height } = this.state.boundingRect; - - return ( - - ); - } - - renderAxis(timelineTransform) { - const { width, height } = this.state.boundingRect; - - return ( - - - {this.renderDisabledShadow(timelineTransform)} - - {this.renderPeriodTicks('year', timelineTransform)} - {this.renderPeriodTicks('month', timelineTransform)} - {this.renderPeriodTicks('day', timelineTransform)} - {this.renderPeriodTicks('minute', timelineTransform)} - - - ); - } - - renderAnimatedContent() { - const focusedTimestampValue = this.state.focusedTimestamp.valueOf(); - const durationPerPixelValue = this.state.durationPerPixel.asMilliseconds(); - - return ( - - {interpolated => this.renderAxis({ - focusedTimestamp: moment(interpolated.focusedTimestampValue), - durationPerPixel: moment.duration(interpolated.durationPerPixelValue), - })} - - ); - } - - render() { - const className = classNames({ panning: this.state.isPanning }); - const halfWidth = this.state.boundingRect.width / 2; - - return ( -
- - - - - - Scroll to zoom, drag to pan - {this.renderAnimatedContent()} - - - - - -
- ); - } -} diff --git a/client/app/scripts/components/time-travel.js b/client/app/scripts/components/time-travel.js index 0a31901b2..d52b4b5ab 100644 --- a/client/app/scripts/components/time-travel.js +++ b/client/app/scripts/components/time-travel.js @@ -3,10 +3,8 @@ import { connect } from 'react-redux'; import TimeTravelComponent from './time-travel-component'; import { trackAnalyticsEvent } from '../utils/tracking-utils'; -import { - jumpToTime, - timeTravelStartTransition, -} from '../actions/app-actions'; +import { jumpToTime } from '../actions/app-actions'; + class TimeTravel extends React.Component { constructor(props, context) { @@ -15,11 +13,11 @@ class TimeTravel extends React.Component { this.changeTimestamp = this.changeTimestamp.bind(this); this.trackTimestampEdit = this.trackTimestampEdit.bind(this); this.trackTimelineClick = this.trackTimelineClick.bind(this); + this.trackTimelineZoom = this.trackTimelineZoom.bind(this); this.trackTimelinePan = this.trackTimelinePan.bind(this); } changeTimestamp(timestamp) { - this.props.timeTravelStartTransition(); this.props.jumpToTime(timestamp); } @@ -47,6 +45,15 @@ class TimeTravel extends React.Component { }); } + trackTimelineZoom(zoomedPeriod) { + trackAnalyticsEvent('scope.time.timeline.zoom', { + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + zoomedPeriod, + }); + } + render() { const { visible, timestamp, viewportWidth } = this.props; @@ -58,6 +65,7 @@ class TimeTravel extends React.Component { changeTimestamp={this.changeTimestamp} trackTimestampEdit={this.trackTimestampEdit} trackTimelineClick={this.trackTimelineClick} + trackTimelineZoom={this.trackTimelineZoom} trackTimelinePan={this.trackTimelinePan} /> ); @@ -77,5 +85,5 @@ function mapStateToProps(state) { export default connect( mapStateToProps, - { jumpToTime, timeTravelStartTransition }, + { jumpToTime }, )(TimeTravel); diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index 5238ad391..7ae350ae2 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -63,7 +63,6 @@ const ACTION_TYPES = [ 'SHUTDOWN', 'SORT_ORDER_CHANGED', 'START_TIME_TRAVEL', - 'TIME_TRAVEL_START_TRANSITION', 'TOGGLE_CONTRAST_MODE', 'TOGGLE_TROUBLESHOOTING_MENU', 'UNHOVER_METRIC', diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index bf38e412a..fc558d118 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -379,13 +379,10 @@ export function rootReducer(state = initialState, action) { } case ActionTypes.JUMP_TO_TIME: { + state = state.set('timeTravelTransitioning', true); return state.set('pausedAt', action.timestamp); } - case ActionTypes.TIME_TRAVEL_START_TRANSITION: { - return state.set('timeTravelTransitioning', true); - } - case ActionTypes.FINISH_TIME_TRAVEL_TRANSITION: { state = state.set('timeTravelTransitioning', false); return clearNodes(state); From 11b2e77898828481e6834325ceba1e06dc8bc64b Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 16 Oct 2017 15:58:48 +0200 Subject: [PATCH 4/7] Using styled components. --- client/package.json | 1 + client/yarn.lock | 55 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/client/package.json b/client/package.json index aee1066ce..b6ebda8d5 100644 --- a/client/package.json +++ b/client/package.json @@ -43,6 +43,7 @@ "reqwest": "2.0.5", "reselect": "3.0.0", "reselect-map": "1.0.1", + "styled-components": "^2.2.1", "weaveworks-ui-components": "git+https://github.com/weaveworks/ui-components.git#v0.1.28", "whatwg-fetch": "2.0.3", "xterm": "2.5.0" diff --git a/client/yarn.lock b/client/yarn.lock index 4f8e833d6..a9d4a1526 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1107,6 +1107,13 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" +buffer@^5.0.3: + version "5.0.8" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.0.8.tgz#84daa52e7cf2fa8ce4195bc5cf0f7809e0930b24" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1543,6 +1550,10 @@ css-animation@^1.3.0: dependencies: component-classes "^1.2.5" +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + css-color-names@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" @@ -1590,6 +1601,14 @@ css-selector-tokenizer@^0.7.0: fastparse "^1.1.1" regexpu-core "^1.0.0" +css-to-react-native@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-2.0.4.tgz#cf4cc407558b3474d4ba8be1a2cd3b6ce713101b" + dependencies: + css-color-keywords "^1.0.0" + fbjs "^0.8.5" + postcss-value-parser "^3.3.0" + css-what@2.1: version "2.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd" @@ -2425,6 +2444,18 @@ fbjs@^0.8.4, fbjs@^0.8.9: setimmediate "^1.0.5" ua-parser-js "^0.7.9" +fbjs@^0.8.5: + version "0.8.16" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + figures@^1.3.5: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -2851,7 +2882,7 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" -hoist-non-react-statics@^1.0.3: +hoist-non-react-statics@^1.0.3, hoist-non-react-statics@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" @@ -3156,6 +3187,10 @@ is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" +is-function@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" + is-generator-function@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.6.tgz#9e71653cd15fff341c79c4151460a131d31e9fc4" @@ -5818,6 +5853,24 @@ style-loader@0.17.0: dependencies: loader-utils "^1.0.2" +styled-components@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.2.1.tgz#f4835f1001c37bcc301ac3865b5d93466de4dd5b" + dependencies: + buffer "^5.0.3" + css-to-react-native "^2.0.3" + fbjs "^0.8.9" + hoist-non-react-statics "^1.2.0" + is-function "^1.0.1" + is-plain-object "^2.0.1" + prop-types "^15.5.4" + stylis "^3.2.1" + supports-color "^3.2.3" + +stylis@^3.2.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.3.2.tgz#95ef285836e98243f8b8f64a9a72706ea6c893ea" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" From 8a5eb637a689b2d29c7fd78e44cccfbdbf787db3 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 16 Oct 2017 16:26:50 +0200 Subject: [PATCH 5/7] Inject ui-components Theme into App component. --- client/app/scripts/components/app.js | 63 +++++++++++++++------------- client/package.json | 2 +- client/yarn.lock | 50 ++++++++++++++++------ 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index eda6590c7..8a6ebd167 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -4,6 +4,9 @@ import classNames from 'classnames'; import { connect } from 'react-redux'; import { debounce } from 'lodash'; +import { ThemeProvider } from 'styled-components'; +import theme from 'weaveworks-ui-components/lib/theme'; + import Logo from './logo'; import Footer from './footer'; import Sidebar from './sidebar'; @@ -175,42 +178,44 @@ class App extends React.Component { const isIframe = window !== window.top; return ( -
- {showingDebugToolbar() && } + +
+ {showingDebugToolbar() && } - {showingHelp && } + {showingHelp && } - {showingTroubleshootingMenu && } + {showingTroubleshootingMenu && } - {showingDetails &&
} + {showingDetails &&
} -
- -
-
- {!isIframe && - - } +
+ +
+
+ {!isIframe && + + } +
+ + + +
- - - -
+ + + + + {showingNetworkSelector && isGraphViewMode && } + {!isResourceViewMode && } + {!isResourceViewMode && } + + +
+ +
- - - - - {showingNetworkSelector && isGraphViewMode && } - {!isResourceViewMode && } - {!isResourceViewMode && } - - -
- - -
+ ); } } diff --git a/client/package.json b/client/package.json index b6ebda8d5..1bfce232b 100644 --- a/client/package.json +++ b/client/package.json @@ -44,7 +44,7 @@ "reselect": "3.0.0", "reselect-map": "1.0.1", "styled-components": "^2.2.1", - "weaveworks-ui-components": "git+https://github.com/weaveworks/ui-components.git#v0.1.28", + "weaveworks-ui-components": "git+https://github.com/weaveworks/ui-components.git#v0.1.45", "whatwg-fetch": "2.0.3", "xterm": "2.5.0" }, diff --git a/client/yarn.lock b/client/yarn.lock index a9d4a1526..9074d831d 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -710,6 +710,13 @@ babel-plugin-transform-object-rest-spread@6.23.0: babel-plugin-syntax-object-rest-spread "^6.8.0" babel-runtime "^6.22.0" +babel-plugin-transform-object-rest-spread@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.26.0" + babel-plugin-transform-react-display-name@^6.23.0, babel-plugin-transform-react-display-name@^6.3.13: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.23.0.tgz#4398910c358441dc4cef18787264d0412ed36b37" @@ -871,6 +878,13 @@ babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6 core-js "^2.4.0" regenerator-runtime "^0.10.0" +babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + babel-template@^6.16.0, babel-template@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.24.1.tgz#04ae514f1f93b3a2537f2a0f60a5a45fb8308333" @@ -4235,9 +4249,9 @@ node-pre-gyp@^0.6.29: tar "^2.2.1" tar-pack "^3.4.0" -node-sass@3.13.0: - version "3.13.0" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-3.13.0.tgz#d08b95bdebf40941571bd2c16a9334b980f8924f" +node-sass@4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.2.tgz#4012fa2bd129b1d6365117e88d9da0500d99da64" dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -4248,17 +4262,19 @@ node-sass@3.13.0: in-publish "^2.0.0" lodash.assign "^4.2.0" lodash.clonedeep "^4.3.2" + lodash.mergewith "^4.6.0" meow "^3.7.0" mkdirp "^0.5.1" nan "^2.3.2" node-gyp "^3.3.1" npmlog "^4.0.0" - request "^2.61.0" + request "^2.79.0" sass-graph "^2.1.1" + stdout-stream "^1.4.0" -node-sass@4.5.2: - version "4.5.2" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.2.tgz#4012fa2bd129b1d6365117e88d9da0500d99da64" +node-sass@4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.3.tgz#d09c9d1179641239d1b97ffc6231fdcec53e1568" dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -4630,6 +4646,10 @@ pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" +polished@^1.7.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/polished/-/polished-1.8.1.tgz#e50b9f789d37d98da64912f1be2bf759d8bfae6c" + postcss-calc@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" @@ -5323,6 +5343,10 @@ regenerator-runtime@^0.10.0: version "0.10.3" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.3.tgz#8c4367a904b51ea62a908ac310bf99ff90a82a3e" +regenerator-runtime@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1" + regenerator-transform@0.9.11: version "0.9.11" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.11.tgz#3a7d067520cb7b7176769eb5ff868691befe1283" @@ -5392,7 +5416,7 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request@2, request@^2.53.0, request@^2.61.0, request@^2.65.0, request@^2.72.0, request@^2.79.0, request@^2.81.0: +request@2, request@^2.53.0, request@^2.65.0, request@^2.72.0, request@^2.79.0, request@^2.81.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" dependencies: @@ -6224,16 +6248,18 @@ 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.28": - version "0.1.28" - resolved "git+https://github.com/weaveworks/ui-components.git#164a0d2770ee5c2318b2f1ab1948fb6046059909" +"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" dependencies: babel-cli "^6.18.0" babel-plugin-transform-export-extensions "6.8.0" + babel-plugin-transform-object-rest-spread "^6.26.0" babel-preset-es2015 "6.18.0" babel-preset-react "6.16.0" classnames "^2.2.5" - node-sass "3.13.0" + node-sass "4.5.3" + polished "^1.7.0" prop-types "^15.5.8" webidl-conversions@^3.0.0: From 0056424357d87a8a7863bc860d3d521f95382f36 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 16 Oct 2017 20:01:49 +0200 Subject: [PATCH 6/7] TimeTravelComponent as a fully styled component. --- .../components/time-travel-component.js | 157 +++++++++++++++--- client/app/styles/_base.scss | 121 ++------------ 2 files changed, 143 insertions(+), 135 deletions(-) diff --git a/client/app/scripts/components/time-travel-component.js b/client/app/scripts/components/time-travel-component.js index e270bc095..cf38d5764 100644 --- a/client/app/scripts/components/time-travel-component.js +++ b/client/app/scripts/components/time-travel-component.js @@ -1,6 +1,6 @@ import React from 'react'; import moment from 'moment'; -import classNames from 'classnames'; +import styled from 'styled-components'; import { map, clamp, find, last, debounce } from 'lodash'; import { drag } from 'd3-drag'; import { scaleUtc } from 'd3-scale'; @@ -64,6 +64,7 @@ const TICK_SETTINGS_PER_PERIOD = { }, }; +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'); @@ -74,6 +75,117 @@ 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 { + content: ''; + position: absolute; + display: block; + left: 50%; + border: 1px solid white; + border-top: 0; + border-bottom: 0; + background-color: red; + 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 #aaa; + pointer-events: all; + margin: 0 7px; +`; + +const DisabledRange = styled.rect` + fill: #888; + fill-opacity: 0.1; +`; + +const TimestampLabel = styled.a` + margin-left: 2px; + padding: 3px; + + &[disabled] { + color: #aaa; + cursor: inherit; + } +`; + +const TimelinePanButton = styled.a` + pointer-events: all; + padding: 2px; +`; + +const TimestampContainer = styled.div` + background-color: ${props => props.theme.colors.white}; + border: 1px solid #ccc; + 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` + border: 0; + background-color: transparent; + font-size: 1rem; + width: 165px; + font-family: "Roboto", sans-serif; + text-align: center; + outline: 0; +`; + + function getTimeScale({ focusedTimestamp, durationPerPixel }) { const roundedTimestamp = moment(focusedTimestamp).utc().startOf('second'); const startDate = moment(roundedTimestamp).subtract(durationPerPixel); @@ -342,9 +454,9 @@ export default class TimeTravelComponent extends React.Component { {!isBehind && } {!disabled && Jump to {timestamp.utc().format()}} - + {timestamp.utc().format(periodFormat)} - + ); @@ -377,11 +489,7 @@ export default class TimeTravelComponent extends React.Component { const { width, height } = this.state.boundingRect; return ( - + ); } @@ -425,33 +533,32 @@ export default class TimeTravelComponent extends React.Component { } render() { - const { visible } = this.props; - const className = classNames({ panning: this.state.isPanning }); - const halfWidth = this.state.boundingRect.width / 2; + const { isPanning, boundingRect } = this.state; + const halfWidth = boundingRect.width / 2; return ( -
-
- + + + - - - + + + Scroll to zoom, drag to pan {this.renderAnimatedContent()} - - + + - -
-
- + + + UTC -
-
+ + ); } } diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 0834a3b81..25822eda3 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -51,24 +51,21 @@ a { } // From https://stackoverflow.com/a/18294634 -.grabbable { - cursor: move; /* fallback if grab cursor is unsupported */ - cursor: grab; - cursor: -moz-grab; - cursor: -webkit-grab; -} -.grabbing { - cursor: grabbing; - cursor: -moz-grabbing; - cursor: -webkit-grabbing; -} - .fully-pannable { width: 100%; height: 100%; - @extend .grabbable; + // Grabbable + cursor: move; /* fallback if grab cursor is unsupported */ + cursor: grab; + cursor: -moz-grab; + cursor: -webkit-grab; - &.panning { @extend .grabbing; } + &.panning { + // Grabbing + cursor: grabbing; + cursor: -moz-grabbing; + cursor: -webkit-grabbing; + } } .shadow-2 { @@ -308,102 +305,6 @@ a { } } -.time-travel { - position: relative; - margin-bottom: 15px; - z-index: 2001; - - transition: all .15s $base-ease; - overflow: hidden; - height: 0; - - &.visible { - height: $timeline-height + 35px; - margin-bottom: 15px; - margin-top: -5px; - } - - .button { - padding: 2px; - pointer-events: all; - } - - .time-travel-timeline { - align-items: center; - display: flex; - height: $timeline-height; - - svg { - @extend .fully-pannable; - background-color: rgba(255, 255, 255, 0.85); - box-shadow: inset 0 0 7px #aaa; - pointer-events: all; - margin: 0 7px; - - .available-range { - fill: #888; - fill-opacity: 0.1; - } - - .timestamp-label { - margin-left: 2px; - padding: 3px; - - &[disabled] { - color: #aaa; - cursor: inherit; - } - } - } - - &:before, &:after { - content: ''; - position: absolute; - display: block; - left: 50%; - border: 1px solid white; - border-top: 0; - border-bottom: 0; - background-color: red; - margin-left: -1px; - width: 3px; - } - - &:before { - top: 0; - height: $timeline-height; - } - - &:after { - top: $timeline-height; - height: 9px; - opacity: 0.15; - } - } - - &-timestamp { - background-color: $background-lighter-color; - border: 1px solid #ccc; - border-radius: 4px; - padding: 2px 8px; - pointer-events: all; - margin: 8px 0 25px 50%; - transform: translateX(-50%); - opacity: 0.8; - display: inline-block; - - input { - border: 0; - background-color: transparent; - font-size: 1rem; - width: 165px; - font-family: "Roboto", sans-serif; - text-align: center; - outline: 0; - } - } -} - .zoomable-canvas svg { @extend .fully-pannable; } From 760f1d919ece957326ebe5f35e3e270607d891ca Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 16 Oct 2017 20:19:55 +0200 Subject: [PATCH 7/7] Use more colors from the theme in TimeTravelComponent. --- client/app/scripts/components/time-control.js | 6 ++-- .../components/time-travel-component.js | 31 +++++++++---------- client/app/scripts/constants/timer.js | 4 --- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/client/app/scripts/components/time-control.js b/client/app/scripts/components/time-control.js index 8c231182d..62edd37ae 100644 --- a/client/app/scripts/components/time-control.js +++ b/client/app/scripts/components/time-control.js @@ -6,8 +6,6 @@ import { connect } from 'react-redux'; import { trackAnalyticsEvent } from '../utils/tracking-utils'; import { pauseTimeAtNow, resumeTime, startTimeTravel } from '../actions/app-actions'; -import { TIMELINE_TICK_INTERVAL } from '../constants/timer'; - const className = isSelected => ( classNames('time-control-action', { 'time-control-action-selected': isSelected }) @@ -24,8 +22,8 @@ class TimeControl extends React.Component { } componentDidMount() { - // Force periodic for the paused info. - this.timer = setInterval(() => { this.forceUpdate(); }, TIMELINE_TICK_INTERVAL); + // Force periodic updates every one second for the paused info. + this.timer = setInterval(() => { this.forceUpdate(); }, 1000); } componentWillUnmount() { diff --git a/client/app/scripts/components/time-travel-component.js b/client/app/scripts/components/time-travel-component.js index cf38d5764..6c0d7c8a9 100644 --- a/client/app/scripts/components/time-travel-component.js +++ b/client/app/scripts/components/time-travel-component.js @@ -17,13 +17,6 @@ import { } from '../utils/time-utils'; -import { - TIMELINE_DEBOUNCE_INTERVAL, - TIMELINE_TICK_INTERVAL, - ZOOM_TRACK_DEBOUNCE_INTERVAL, -} from '../constants/timer'; - - const TICK_SETTINGS_PER_PERIOD = { year: { format: 'YYYY', @@ -64,6 +57,10 @@ const TICK_SETTINGS_PER_PERIOD = { }, }; +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'); @@ -112,14 +109,14 @@ const TimelineContainer = styled.div` 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: 1px solid white; border-top: 0; border-bottom: 0; - background-color: red; margin-left: -1px; width: 3px; } @@ -138,14 +135,14 @@ const TimelineContainer = styled.div` const Timeline = FullyPannableCanvas.extend` background-color: rgba(255, 255, 255, 0.85); - box-shadow: inset 0 0 7px #aaa; + box-shadow: inset 0 0 7px ${props => props.theme.colors.gray}; pointer-events: all; margin: 0 7px; `; const DisabledRange = styled.rect` - fill: #888; - fill-opacity: 0.1; + fill: ${props => props.theme.colors.gray}; + fill-opacity: 0.15; `; const TimestampLabel = styled.a` @@ -153,7 +150,7 @@ const TimestampLabel = styled.a` padding: 3px; &[disabled] { - color: #aaa; + color: ${props => props.theme.colors.gray}; cursor: inherit; } `; @@ -165,7 +162,7 @@ const TimelinePanButton = styled.a` const TimestampContainer = styled.div` background-color: ${props => props.theme.colors.white}; - border: 1px solid #ccc; + border: 1px solid ${props => props.theme.colors.gray}; border-radius: 4px; padding: 2px 8px; pointer-events: all; @@ -176,12 +173,12 @@ const TimestampContainer = styled.div` `; const TimestampInput = styled.input` - border: 0; background-color: transparent; - font-size: 1rem; - width: 165px; font-family: "Roboto", sans-serif; text-align: center; + font-size: 1rem; + width: 165px; + border: 0; outline: 0; `; diff --git a/client/app/scripts/constants/timer.js b/client/app/scripts/constants/timer.js index 887f3df9b..81c6f8fbf 100644 --- a/client/app/scripts/constants/timer.js +++ b/client/app/scripts/constants/timer.js @@ -8,7 +8,3 @@ export const TABLE_ROW_FOCUS_DEBOUNCE_INTERVAL = 10; export const VIEWPORT_RESIZE_DEBOUNCE_INTERVAL = 200; export const ZOOM_CACHE_DEBOUNCE_INTERVAL = 500; -export const ZOOM_TRACK_DEBOUNCE_INTERVAL = 10000; - -export const TIMELINE_DEBOUNCE_INTERVAL = 500; -export const TIMELINE_TICK_INTERVAL = 1000;