mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-02 17:50:39 +00:00
Merge pull request #2888 from weaveworks/make-time-travel-modular-component
Make Time Travel a modular component
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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 (
|
||||
<div className={className} ref={this.saveAppRef}>
|
||||
{showingDebugToolbar() && <DebugToolbar />}
|
||||
<ThemeProvider theme={theme}>
|
||||
<div className={className} ref={this.saveAppRef}>
|
||||
{showingDebugToolbar() && <DebugToolbar />}
|
||||
|
||||
{showingHelp && <HelpPanel />}
|
||||
{showingHelp && <HelpPanel />}
|
||||
|
||||
{showingTroubleshootingMenu && <TroubleshootingMenu />}
|
||||
{showingTroubleshootingMenu && <TroubleshootingMenu />}
|
||||
|
||||
{showingDetails && <Details />}
|
||||
{showingDetails && <Details />}
|
||||
|
||||
<div className="header">
|
||||
<TimeTravel />
|
||||
<div className="selectors">
|
||||
<div className="logo">
|
||||
{!isIframe && <svg width="100%" height="100%" viewBox="0 0 1089 217">
|
||||
<Logo />
|
||||
</svg>}
|
||||
<div className="header">
|
||||
<TimeTravel />
|
||||
<div className="selectors">
|
||||
<div className="logo">
|
||||
{!isIframe && <svg width="100%" height="100%" viewBox="0 0 1089 217">
|
||||
<Logo />
|
||||
</svg>}
|
||||
</div>
|
||||
<Search />
|
||||
<Topologies />
|
||||
<ViewModeSelector />
|
||||
<TimeControl />
|
||||
</div>
|
||||
<Search />
|
||||
<Topologies />
|
||||
<ViewModeSelector />
|
||||
<TimeControl />
|
||||
</div>
|
||||
|
||||
<Nodes />
|
||||
|
||||
<Sidebar classNames={isTableViewMode ? 'sidebar-gridmode' : ''}>
|
||||
{showingNetworkSelector && isGraphViewMode && <NetworkSelector />}
|
||||
{!isResourceViewMode && <Status />}
|
||||
{!isResourceViewMode && <TopologyOptions />}
|
||||
</Sidebar>
|
||||
|
||||
<Footer />
|
||||
|
||||
<Overlay faded={timeTravelTransitioning} />
|
||||
</div>
|
||||
|
||||
<Nodes />
|
||||
|
||||
<Sidebar classNames={isTableViewMode ? 'sidebar-gridmode' : ''}>
|
||||
{showingNetworkSelector && isGraphViewMode && <NetworkSelector />}
|
||||
{!isResourceViewMode && <Status />}
|
||||
{!isResourceViewMode && <TopologyOptions />}
|
||||
</Sidebar>
|
||||
|
||||
<Footer />
|
||||
|
||||
<Overlay faded={timeTravelTransitioning} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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 { connect } from 'react-redux';
|
||||
import { drag } from 'd3-drag';
|
||||
import { scaleUtc } from 'd3-scale';
|
||||
import { event as d3Event, select } from 'd3-selection';
|
||||
@@ -11,15 +10,12 @@ import { Motion } from 'react-motion';
|
||||
import { zoomFactor } from '../utils/zoom-utils';
|
||||
import { strongSpring } from '../utils/animation-utils';
|
||||
import { linearGradientValue } from '../utils/math-utils';
|
||||
import { trackAnalyticsEvent } from '../utils/tracking-utils';
|
||||
import {
|
||||
nowInSecondsPrecision,
|
||||
clampToNowInSecondsPrecision,
|
||||
scaleDuration,
|
||||
} from '../utils/time-utils';
|
||||
|
||||
import { TIMELINE_TICK_INTERVAL, ZOOM_TRACK_DEBOUNCE_INTERVAL } from '../constants/timer';
|
||||
|
||||
|
||||
const TICK_SETTINGS_PER_PERIOD = {
|
||||
year: {
|
||||
@@ -61,6 +57,11 @@ 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');
|
||||
const MAX_DURATION_PER_PX = moment.duration(3, 'days');
|
||||
@@ -71,6 +72,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 {
|
||||
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 TimestampLabel = styled.a`
|
||||
margin-left: 2px;
|
||||
padding: 3px;
|
||||
|
||||
&[disabled] {
|
||||
color: ${props => props.theme.colors.gray};
|
||||
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 ${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);
|
||||
@@ -85,8 +197,13 @@ function findOptimalDurationFit(durations, { durationPerPixel }) {
|
||||
return find(durations, d => d >= minimalDuration);
|
||||
}
|
||||
|
||||
function getInputValue(timestamp) {
|
||||
return {
|
||||
inputValue: (timestamp ? moment(timestamp) : moment()).utc().format(),
|
||||
};
|
||||
}
|
||||
|
||||
class TimeTravelTimeline extends React.Component {
|
||||
export default class TimeTravelComponent extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
@@ -96,6 +213,7 @@ class TimeTravelTimeline extends React.Component {
|
||||
durationPerPixel: INIT_DURATION_PER_PX,
|
||||
boundingRect: { width: 0, height: 0 },
|
||||
isPanning: false,
|
||||
...getInputValue(props.timestamp),
|
||||
};
|
||||
|
||||
this.jumpRelativePixels = this.jumpRelativePixels.bind(this);
|
||||
@@ -110,6 +228,15 @@ class TimeTravelTimeline extends React.Component {
|
||||
|
||||
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() {
|
||||
@@ -131,23 +258,37 @@ class TimeTravelTimeline extends React.Component {
|
||||
}
|
||||
|
||||
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.pausedAt) {
|
||||
this.setState({ focusedTimestamp: nextProps.pausedAt });
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
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() {
|
||||
@@ -155,7 +296,7 @@ class TimeTravelTimeline extends React.Component {
|
||||
}
|
||||
|
||||
handlePanEnd() {
|
||||
this.props.onTimelinePanEnd(this.state.focusedTimestamp);
|
||||
this.handleTimelinePanEnd(this.state.focusedTimestamp);
|
||||
this.setState({ isPanning: false });
|
||||
}
|
||||
|
||||
@@ -163,7 +304,7 @@ class TimeTravelTimeline extends React.Component {
|
||||
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.handleTimelinePan(focusedTimestamp);
|
||||
this.setState({ focusedTimestamp });
|
||||
}
|
||||
|
||||
@@ -177,9 +318,31 @@ class TimeTravelTimeline extends React.Component {
|
||||
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.props.onInstantJump(focusedTimestamp);
|
||||
this.handleInstantJump(focusedTimestamp);
|
||||
this.setState({ focusedTimestamp });
|
||||
}
|
||||
|
||||
@@ -288,9 +451,9 @@ class TimeTravelTimeline extends React.Component {
|
||||
{!isBehind && <line y2="75" stroke="#ddd" strokeWidth="1" />}
|
||||
{!disabled && <title>Jump to {timestamp.utc().format()}</title>}
|
||||
<foreignObject width="100" height="20" style={{ lineHeight: '20px' }}>
|
||||
<a className="timestamp-label" disabled={disabled} onClick={!disabled && handleClick}>
|
||||
<TimestampLabel disabled={disabled} onClick={!disabled && handleClick}>
|
||||
{timestamp.utc().format(periodFormat)}
|
||||
</a>
|
||||
</TimestampLabel>
|
||||
</foreignObject>
|
||||
</g>
|
||||
);
|
||||
@@ -323,11 +486,7 @@ class TimeTravelTimeline extends React.Component {
|
||||
const { width, height } = this.state.boundingRect;
|
||||
|
||||
return (
|
||||
<rect
|
||||
className="available-range"
|
||||
transform={`translate(${nowShift}, 0)`}
|
||||
width={width} height={height}
|
||||
/>
|
||||
<DisabledRange transform={`translate(${nowShift}, 0)`} width={width} height={height} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -371,35 +530,32 @@ class TimeTravelTimeline extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
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 (
|
||||
<div className="time-travel-timeline">
|
||||
<a className="button jump-backward" onClick={this.jumpBackward}>
|
||||
<span className="fa fa-chevron-left" />
|
||||
</a>
|
||||
<svg className={className} ref={this.saveSvgRef} onWheel={this.handleZoom}>
|
||||
<g className="view" transform={`translate(${halfWidth}, 0)`}>
|
||||
<title>Scroll to zoom, drag to pan</title>
|
||||
{this.renderAnimatedContent()}
|
||||
</g>
|
||||
</svg>
|
||||
<a className="button jump-forward" onClick={this.jumpForward}>
|
||||
<span className="fa fa-chevron-right" />
|
||||
</a>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
// Used only to trigger recalculations on window resize.
|
||||
viewportWidth: state.getIn(['viewport', 'width']),
|
||||
pausedAt: state.get('pausedAt'),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(TimeTravelTimeline);
|
||||
@@ -1,85 +1,24 @@
|
||||
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 TimeTravelComponent from './time-travel-component';
|
||||
import { trackAnalyticsEvent } from '../utils/tracking-utils';
|
||||
import { clampToNowInSecondsPrecision } from '../utils/time-utils';
|
||||
import {
|
||||
jumpToTime,
|
||||
resumeTime,
|
||||
timeTravelStartTransition,
|
||||
} from '../actions/app-actions';
|
||||
import { jumpToTime } from '../actions/app-actions';
|
||||
|
||||
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.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);
|
||||
|
||||
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();
|
||||
}
|
||||
changeTimestamp(timestamp) {
|
||||
this.props.jumpToTime(timestamp);
|
||||
}
|
||||
|
||||
trackTimestampEdit() {
|
||||
@@ -106,39 +45,45 @@ 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;
|
||||
|
||||
return (
|
||||
<div className={classNames('time-travel', { visible: this.props.showingTimeTravel })}>
|
||||
<TimeTravelTimeline
|
||||
onTimelinePan={this.handleTimelinePan}
|
||||
onTimelinePanEnd={this.handleTimelinePanEnd}
|
||||
onInstantJump={this.handleInstantJump}
|
||||
/>
|
||||
<div className="time-travel-timestamp">
|
||||
<input
|
||||
value={this.state.inputValue}
|
||||
onChange={this.handleInputChange}
|
||||
/> UTC
|
||||
</div>
|
||||
</div>
|
||||
<TimeTravelComponent
|
||||
visible={visible}
|
||||
timestamp={timestamp}
|
||||
viewportWidth={viewportWidth}
|
||||
changeTimestamp={this.changeTimestamp}
|
||||
trackTimestampEdit={this.trackTimestampEdit}
|
||||
trackTimelineClick={this.trackTimelineClick}
|
||||
trackTimelineZoom={this.trackTimelineZoom}
|
||||
trackTimelinePan={this.trackTimelinePan}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
showingTimeTravel: state.get('showingTimeTravel'),
|
||||
visible: state.get('showingTimeTravel'),
|
||||
topologyViewMode: state.get('topologyViewMode'),
|
||||
currentTopology: state.get('currentTopology'),
|
||||
pausedAt: state.get('pausedAt'),
|
||||
timestamp: state.get('pausedAt'),
|
||||
// Used only to trigger recalculations on window resize.
|
||||
viewportWidth: state.getIn(['viewport', 'width']),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
jumpToTime,
|
||||
resumeTime,
|
||||
timeTravelStartTransition,
|
||||
}
|
||||
{ jumpToTime },
|
||||
)(TimeTravel);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
"reqwest": "2.0.5",
|
||||
"reselect": "3.0.0",
|
||||
"reselect-map": "1.0.1",
|
||||
"weaveworks-ui-components": "git+https://github.com/weaveworks/ui-components.git#v0.1.28",
|
||||
"styled-components": "^2.2.1",
|
||||
"weaveworks-ui-components": "git+https://github.com/weaveworks/ui-components.git#v0.1.45",
|
||||
"whatwg-fetch": "2.0.3",
|
||||
"xterm": "2.5.0"
|
||||
},
|
||||
|
||||
105
client/yarn.lock
105
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"
|
||||
@@ -1107,6 +1121,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 +1564,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 +1615,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 +2458,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 +2896,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 +3201,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"
|
||||
@@ -4200,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"
|
||||
@@ -4213,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"
|
||||
@@ -4595,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"
|
||||
@@ -5288,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"
|
||||
@@ -5357,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:
|
||||
@@ -5818,6 +5877,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"
|
||||
@@ -6171,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:
|
||||
|
||||
Reference in New Issue
Block a user