Merge pull request #2888 from weaveworks/make-time-travel-modular-component

Make Time Travel a modular component
This commit is contained in:
Filip Barl
2017-10-17 18:36:22 +02:00
committed by GitHub
11 changed files with 382 additions and 311 deletions

View File

@@ -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({

View File

@@ -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>
);
}
}

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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',

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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"
},

View File

@@ -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: