mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 02:00:43 +00:00
Always show time travel.
This commit is contained in:
@@ -339,14 +339,15 @@ export function clickNode(nodeId, label, origin, topologyId = null) {
|
||||
|
||||
export function pauseTimeAtNow() {
|
||||
return (dispatch, getState) => {
|
||||
const getScopeState = () => getState().scope || getState();
|
||||
dispatch({
|
||||
type: ActionTypes.PAUSE_TIME_AT_NOW
|
||||
});
|
||||
updateRoute(getState);
|
||||
if (!getState().get('nodesLoaded')) {
|
||||
getNodes(getState, dispatch);
|
||||
if (isResourceViewModeSelector(getState())) {
|
||||
getResourceViewNodesSnapshot(getState(), dispatch);
|
||||
updateRoute(getScopeState);
|
||||
if (!getScopeState().get('nodesLoaded')) {
|
||||
getNodes(getScopeState, dispatch);
|
||||
if (isResourceViewModeSelector(getScopeState())) {
|
||||
getResourceViewNodesSnapshot(getScopeState(), dispatch);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -549,7 +550,8 @@ export function receiveNodeDetails(details, requestTimestamp) {
|
||||
|
||||
export function receiveNodesDelta(delta) {
|
||||
return (dispatch, getState) => {
|
||||
if (!isPausedSelector(getState())) {
|
||||
const getScopeState = () => getState().scope || getState();
|
||||
if (!isPausedSelector(getScopeState())) {
|
||||
// Allow css-animation to run smoothly by scheduling it to run on the
|
||||
// next tick after any potentially expensive canvas re-draws have been
|
||||
// completed.
|
||||
@@ -559,7 +561,7 @@ export function receiveNodesDelta(delta) {
|
||||
// only when the first batch of nodes delta has been received. We
|
||||
// do that because we want to keep the previous state blurred instead
|
||||
// of transitioning over an empty state like when switching topologies.
|
||||
if (getState().get('timeTravelTransitioning')) {
|
||||
if (getScopeState().get('timeTravelTransitioning')) {
|
||||
dispatch({ type: ActionTypes.FINISH_TIME_TRAVEL_TRANSITION });
|
||||
}
|
||||
|
||||
@@ -576,16 +578,17 @@ export function receiveNodesDelta(delta) {
|
||||
|
||||
export function resumeTime() {
|
||||
return (dispatch, getState) => {
|
||||
if (isPausedSelector(getState())) {
|
||||
const getScopeState = () => getState().scope || getState();
|
||||
if (isPausedSelector(getScopeState())) {
|
||||
dispatch({
|
||||
type: ActionTypes.RESUME_TIME
|
||||
});
|
||||
updateRoute(getState);
|
||||
updateRoute(getScopeState);
|
||||
// After unpausing, all of the following calls will re-activate polling.
|
||||
getTopologies(getState, dispatch);
|
||||
getNodes(getState, dispatch, true);
|
||||
if (isResourceViewModeSelector(getState())) {
|
||||
getResourceViewNodesSnapshot(getState(), dispatch);
|
||||
getTopologies(getScopeState, dispatch);
|
||||
getNodes(getScopeState, dispatch, true);
|
||||
if (isResourceViewModeSelector(getScopeState())) {
|
||||
getResourceViewNodesSnapshot(getScopeState(), dispatch);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@ import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-t
|
||||
import { getRouter, getUrlState } from '../utils/router-utils';
|
||||
import { trackAnalyticsEvent } from '../utils/tracking-utils';
|
||||
import { availableNetworksSelector } from '../selectors/node-networks';
|
||||
import { timeTravelSupportedSelector } from '../selectors/time-travel';
|
||||
import {
|
||||
isResourceViewModeSelector,
|
||||
isTableViewModeSelector,
|
||||
@@ -177,10 +178,10 @@ class App extends React.Component {
|
||||
const {
|
||||
isTableViewMode, isGraphViewMode, isResourceViewMode, showingDetails,
|
||||
showingHelp, showingNetworkSelector, showingTroubleshootingMenu,
|
||||
timeTravelTransitioning, showingTimeTravel
|
||||
timeTravelTransitioning, timeTravelSupported
|
||||
} = this.props;
|
||||
|
||||
const className = classNames('scope-app', { 'time-travel-open': showingTimeTravel });
|
||||
const className = classNames('scope-app', { 'time-travel-open': timeTravelSupported });
|
||||
const isIframe = window !== window.top;
|
||||
|
||||
return (
|
||||
@@ -195,9 +196,11 @@ class App extends React.Component {
|
||||
{showingDetails && <Details />}
|
||||
|
||||
<div className="header">
|
||||
<CloudFeature alwaysShow>
|
||||
<TimeTravelWrapper />
|
||||
</CloudFeature>
|
||||
{timeTravelSupported && (
|
||||
<CloudFeature alwaysShow>
|
||||
<TimeTravelWrapper />
|
||||
</CloudFeature>
|
||||
)}
|
||||
|
||||
<div className="selectors">
|
||||
<div className="logo">
|
||||
@@ -244,11 +247,11 @@ function mapStateToProps(state) {
|
||||
searchQuery: state.get('searchQuery'),
|
||||
showingDetails: state.get('nodeDetails').size > 0,
|
||||
showingHelp: state.get('showingHelp'),
|
||||
showingTimeTravel: state.get('showingTimeTravel'),
|
||||
showingTroubleshootingMenu: state.get('showingTroubleshootingMenu'),
|
||||
showingNetworkSelector: availableNetworksSelector(state).count() > 0,
|
||||
showingTerminal: state.get('controlPipes').size > 0,
|
||||
topologyViewMode: state.get('topologyViewMode'),
|
||||
timeTravelSupported: timeTravelSupportedSelector(state),
|
||||
timeTravelTransitioning: state.get('timeTravelTransitioning'),
|
||||
urlState: getUrlState(state)
|
||||
};
|
||||
|
||||
@@ -86,7 +86,6 @@ function mapStateToProps(state) {
|
||||
topologyNodeCountZero: isTopologyNodeCountZero(state),
|
||||
nodesDisplayEmpty: isNodesDisplayEmpty(state),
|
||||
nodesLoaded: nodesLoadedSelector(state),
|
||||
timeTravelTransitioning: state.get('timeTravelTransitioning'),
|
||||
currentTopology: state.get('currentTopology'),
|
||||
topologies: state.get('topologies'),
|
||||
topologiesLoaded: state.get('topologiesLoaded'),
|
||||
|
||||
@@ -4,7 +4,8 @@ import classNames from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { trackAnalyticsEvent } from '../utils/tracking-utils';
|
||||
import { pauseTimeAtNow, resumeTime, startTimeTravel } from '../actions/app-actions';
|
||||
import { pauseTimeAtNow, resumeTime } from '../actions/app-actions';
|
||||
import { isPausedSelector, timeTravelSupportedSelector } from '../selectors/time-travel';
|
||||
|
||||
|
||||
const className = isSelected => (
|
||||
@@ -17,7 +18,6 @@ class TimeControl extends React.Component {
|
||||
|
||||
this.handleNowClick = this.handleNowClick.bind(this);
|
||||
this.handlePauseClick = this.handlePauseClick.bind(this);
|
||||
this.handleTravelClick = this.handleTravelClick.bind(this);
|
||||
this.getTrackingMetadata = this.getTrackingMetadata.bind(this);
|
||||
}
|
||||
|
||||
@@ -50,71 +50,44 @@ class TimeControl extends React.Component {
|
||||
this.props.pauseTimeAtNow();
|
||||
}
|
||||
|
||||
handleTravelClick() {
|
||||
if (!this.props.showingTimeTravel) {
|
||||
trackAnalyticsEvent('scope.time.travel.click', this.getTrackingMetadata({ open: true }));
|
||||
this.props.startTimeTravel();
|
||||
} else {
|
||||
trackAnalyticsEvent('scope.time.travel.click', this.getTrackingMetadata({ open: false }));
|
||||
this.props.resumeTime();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
showingTimeTravel, pausedAt, timeTravelTransitioning, topologiesLoaded,
|
||||
hasHistoricReports
|
||||
} = this.props;
|
||||
const { isPaused, pausedAt, topologiesLoaded } = this.props;
|
||||
|
||||
const isPausedNow = pausedAt && !showingTimeTravel;
|
||||
const isTimeTravelling = showingTimeTravel;
|
||||
const isRunningNow = !pausedAt;
|
||||
|
||||
if (!topologiesLoaded) return null;
|
||||
// If Time Travel is supported, show an empty placeholder div instead
|
||||
// of this control, since time will be controlled through the timeline.
|
||||
// We return <div /> instead of null so that selector controls would
|
||||
// be aligned the same way between WC Explore and Scope standalone.
|
||||
if (this.props.timeTravelSupported) return <div />;
|
||||
|
||||
return (
|
||||
<div className="time-control">
|
||||
<div className="time-control-controls">
|
||||
<div className="time-control-spinner">
|
||||
{timeTravelTransitioning && <span className="fa fa-circle-o-notch fa-spin" />}
|
||||
</div>
|
||||
<div className="time-control-wrapper">
|
||||
<span
|
||||
className={className(isRunningNow)}
|
||||
className={className(!isPaused)}
|
||||
onClick={this.handleNowClick}
|
||||
disabled={!topologiesLoaded}
|
||||
title="Show live state of the system">
|
||||
{isRunningNow && <span className="fa fa-play" />}
|
||||
{!isPaused && <span className="fa fa-play" />}
|
||||
<span className="label">Live</span>
|
||||
</span>
|
||||
<span
|
||||
className={className(isPausedNow)}
|
||||
onClick={!isTimeTravelling ? this.handlePauseClick : null}
|
||||
disabled={isTimeTravelling}
|
||||
className={className(isPaused)}
|
||||
onClick={this.handlePauseClick}
|
||||
disabled={!topologiesLoaded}
|
||||
title="Pause updates (freezes the nodes in their current layout)">
|
||||
{isPausedNow && <span className="fa fa-pause" />}
|
||||
<span className="label">{isPausedNow ? 'Paused' : 'Pause'}</span>
|
||||
{isPaused && <span className="fa fa-pause" />}
|
||||
<span className="label">{isPaused ? 'Paused' : 'Pause'}</span>
|
||||
</span>
|
||||
{hasHistoricReports &&
|
||||
<span
|
||||
className={className(isTimeTravelling)}
|
||||
onClick={this.handleTravelClick}
|
||||
title="Travel back in time">
|
||||
{isTimeTravelling && <span className="fa fa-clock-o" />}
|
||||
<span className="label">Time Travel</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{(isPausedNow || isTimeTravelling) &&
|
||||
{isPaused &&
|
||||
<span
|
||||
className="time-control-info"
|
||||
title={moment(pausedAt).toISOString()}>
|
||||
Showing state from {moment(pausedAt).fromNow()}
|
||||
</span>
|
||||
}
|
||||
{isRunningNow && timeTravelTransitioning &&
|
||||
<span className="time-control-info">Resuming the live state</span>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -122,12 +95,11 @@ class TimeControl extends React.Component {
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
hasHistoricReports: state.getIn(['capabilities', 'historic_reports']),
|
||||
isPaused: isPausedSelector(state),
|
||||
timeTravelSupported: timeTravelSupportedSelector(state),
|
||||
topologyViewMode: state.get('topologyViewMode'),
|
||||
topologiesLoaded: state.get('topologiesLoaded'),
|
||||
currentTopology: state.get('currentTopology'),
|
||||
showingTimeTravel: state.get('showingTimeTravel'),
|
||||
timeTravelTransitioning: state.get('timeTravelTransitioning'),
|
||||
pausedAt: state.get('pausedAt'),
|
||||
};
|
||||
}
|
||||
@@ -137,6 +109,5 @@ export default connect(
|
||||
{
|
||||
resumeTime,
|
||||
pauseTimeAtNow,
|
||||
startTimeTravel,
|
||||
}
|
||||
)(TimeControl);
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import styled from 'styled-components';
|
||||
import { connect } from 'react-redux';
|
||||
import { TimeTravel } from 'weaveworks-ui-components';
|
||||
|
||||
import { trackAnalyticsEvent } from '../utils/tracking-utils';
|
||||
import { jumpToTime } from '../actions/app-actions';
|
||||
import { jumpToTime, resumeTime, pauseTimeAtNow } from '../actions/app-actions';
|
||||
|
||||
|
||||
const TimeTravelContainer = styled.div`
|
||||
transition: all .15s ease-in-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
|
||||
${props => props.visible && `
|
||||
height: 105px;
|
||||
`}
|
||||
`;
|
||||
|
||||
class TimeTravelWrapper extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.handleLiveModeChange = this.handleLiveModeChange.bind(this);
|
||||
|
||||
this.trackTimestampEdit = this.trackTimestampEdit.bind(this);
|
||||
this.trackTimelinePanButtonClick = this.trackTimelinePanButtonClick.bind(this);
|
||||
this.trackTimelineLabelClick = this.trackTimelineLabelClick.bind(this);
|
||||
@@ -71,20 +61,29 @@ class TimeTravelWrapper extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleLiveModeChange(showingLive) {
|
||||
if (showingLive) {
|
||||
this.props.resumeTime();
|
||||
} else {
|
||||
this.props.pauseTimeAtNow();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TimeTravelContainer visible={this.props.visible}>
|
||||
<TimeTravel
|
||||
timestamp={this.props.timestamp}
|
||||
earliestTimestamp={this.props.earliestTimestamp}
|
||||
onChangeTimestamp={this.props.jumpToTime}
|
||||
onTimestampInputEdit={this.trackTimestampEdit}
|
||||
onTimelinePanButtonClick={this.trackTimelinePanButtonClick}
|
||||
onTimelineLabelClick={this.trackTimelineLabelClick}
|
||||
onTimelineZoom={this.trackTimelineZoom}
|
||||
onTimelinePan={this.trackTimelinePan}
|
||||
/>
|
||||
</TimeTravelContainer>
|
||||
<TimeTravel
|
||||
hasLiveMode
|
||||
showingLive={this.props.showingLive}
|
||||
onChangeLiveMode={this.handleLiveModeChange}
|
||||
timestamp={this.props.timestamp}
|
||||
earliestTimestamp={this.props.earliestTimestamp}
|
||||
onChangeTimestamp={this.props.jumpToTime}
|
||||
onTimestampInputEdit={this.trackTimestampEdit}
|
||||
onTimelinePanButtonClick={this.trackTimelinePanButtonClick}
|
||||
onTimelineLabelClick={this.trackTimelineLabelClick}
|
||||
onTimelineZoom={this.trackTimelineZoom}
|
||||
onTimelinePan={this.trackTimelinePan}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -102,7 +101,7 @@ function mapStateToProps(state, { params }) {
|
||||
}
|
||||
|
||||
return {
|
||||
visible: scopeState.get('showingTimeTravel'),
|
||||
showingLive: !scopeState.get('pausedAt'),
|
||||
topologyViewMode: scopeState.get('topologyViewMode'),
|
||||
currentTopology: scopeState.get('currentTopology'),
|
||||
earliestTimestamp: firstSeenConnectedAt,
|
||||
@@ -112,5 +111,5 @@ function mapStateToProps(state, { params }) {
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ jumpToTime },
|
||||
{ jumpToTime, resumeTime, pauseTimeAtNow },
|
||||
)(TimeTravelWrapper);
|
||||
|
||||
@@ -75,7 +75,6 @@ export const initialState = makeMap({
|
||||
selectedNetwork: null,
|
||||
selectedNodeId: null,
|
||||
showingHelp: false,
|
||||
showingTimeTravel: false,
|
||||
showingTroubleshootingMenu: false,
|
||||
showingNetworks: false,
|
||||
timeTravelTransitioning: false,
|
||||
@@ -369,18 +368,15 @@ export function rootReducer(state = initialState, action) {
|
||||
|
||||
case ActionTypes.RESUME_TIME: {
|
||||
state = state.set('timeTravelTransitioning', true);
|
||||
state = state.set('showingTimeTravel', false);
|
||||
return state.set('pausedAt', null);
|
||||
}
|
||||
|
||||
case ActionTypes.PAUSE_TIME_AT_NOW: {
|
||||
state = state.set('showingTimeTravel', false);
|
||||
state = state.set('timeTravelTransitioning', false);
|
||||
return state.set('pausedAt', moment().utc().format());
|
||||
}
|
||||
|
||||
case ActionTypes.START_TIME_TRAVEL: {
|
||||
state = state.set('showingTimeTravel', true);
|
||||
state = state.set('timeTravelTransitioning', false);
|
||||
return state.set('pausedAt', action.timestamp || moment().utc().format());
|
||||
}
|
||||
|
||||
@@ -7,3 +7,5 @@ export const isPausedSelector = createSelector(
|
||||
],
|
||||
pausedAt => !!pausedAt
|
||||
);
|
||||
|
||||
export const timeTravelSupportedSelector = state => state.getIn(['capabilities', 'historic_reports']);
|
||||
|
||||
@@ -211,6 +211,7 @@ a {
|
||||
.selectors {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
> * {
|
||||
z-index: 20;
|
||||
flex: 1 1;
|
||||
|
||||
Reference in New Issue
Block a user