Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Time travel redesign #2651

Merged
merged 23 commits into from
Jul 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 77 additions & 96 deletions client/app/scripts/actions/app-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import {
doControlRequest,
getAllNodes,
getResourceViewNodesSnapshot,
updateWebsocketChannel,
getNodeDetails,
getTopologies,
deletePipe,
stopPolling,
teardownWebsockets,
getNodes,
} from '../utils/web-api-utils';
import { storageSet } from '../utils/storage-utils';
import { loadTheme } from '../utils/contrast-utils';
Expand All @@ -29,8 +29,6 @@ import {
resourceViewAvailableSelector,
} from '../selectors/topology';

import { NODES_DELTA_BUFFER_SIZE_LIMIT } from '../constants/limits';
import { NODES_DELTA_BUFFER_FEED_INTERVAL } from '../constants/timer';
import {
GRAPH_VIEW_MODE,
TABLE_VIEW_MODE,
Expand All @@ -40,8 +38,6 @@ import {

const log = debug('scope:app-actions');

// TODO: This shouldn't be exposed here as a global variable.
let nodesDeltaBufferUpdateTimer = null;

export function showHelp() {
return { type: ActionTypes.SHOW_HELP };
Expand Down Expand Up @@ -75,11 +71,6 @@ export function sortOrderChanged(sortedBy, sortedDesc) {
};
}

function resetNodesDeltaBuffer() {
clearInterval(nodesDeltaBufferUpdateTimer);
return { type: ActionTypes.CLEAR_NODES_DELTA_BUFFER };
}


//
// Networks
Expand Down Expand Up @@ -216,11 +207,8 @@ export function changeTopologyOption(option, value, topologyId, addOrRemove) {
});
updateRoute(getState);
// update all request workers with new options
dispatch(resetNodesDeltaBuffer());
const state = getState();
getTopologies(state, dispatch);
updateWebsocketChannel(state, dispatch);
getNodeDetails(state, dispatch);
getTopologies(getState, dispatch);
getNodes(getState, dispatch);
};
}

Expand Down Expand Up @@ -344,13 +332,13 @@ export function clickNode(nodeId, label, origin) {
nodeId
});
updateRoute(getState);
getNodeDetails(getState(), dispatch);
getNodeDetails(getState, dispatch);
};
}

export function clickPauseUpdate() {
export function pauseTimeAtNow() {
return {
type: ActionTypes.CLICK_PAUSE_UPDATE
type: ActionTypes.PAUSE_TIME_AT_NOW
};
}

Expand All @@ -364,7 +352,7 @@ export function clickRelative(nodeId, topologyId, label, origin) {
topologyId
});
updateRoute(getState);
getNodeDetails(getState(), dispatch);
getNodeDetails(getState, dispatch);
};
}

Expand All @@ -375,12 +363,10 @@ function updateTopology(dispatch, getState) {
getResourceViewNodesSnapshot(state, dispatch);
}
updateRoute(getState);
// update all request workers with new options
dispatch(resetNodesDeltaBuffer());
// NOTE: This is currently not needed for our static resource
// view, but we'll need it here later and it's simpler to just
// keep it than to redo the nodes delta updating logic.
updateWebsocketChannel(state, dispatch);
getNodes(getState, dispatch);
}

export function clickShowTopologyForNode(topologyId, nodeId) {
Expand All @@ -404,28 +390,6 @@ export function clickTopology(topologyId) {
};
}

export function timeTravelStartTransition() {
return {
type: ActionTypes.TIME_TRAVEL_START_TRANSITION,
};
}

export function timeTravelJumpToPast(millisecondsInPast) {
return (dispatch, getServiceState) => {
dispatch({
type: ActionTypes.TIME_TRAVEL_MILLISECONDS_IN_PAST,
millisecondsInPast,
});
const scopeState = getServiceState().scope;
updateWebsocketChannel(scopeState, dispatch);
dispatch(resetNodesDeltaBuffer());
getTopologies(getServiceState().scope, dispatch);
if (isResourceViewModeSelector(scopeState)) {
getResourceViewNodesSnapshot(scopeState, dispatch);
}
};
}

export function cacheZoomState(zoomState) {
return {
type: ActionTypes.CACHE_ZOOM_STATE,
Expand Down Expand Up @@ -573,29 +537,22 @@ export function receiveNodeDetails(details) {

export function receiveNodesDelta(delta) {
return (dispatch, getState) => {
//
// 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.
//
setTimeout(() => dispatch({ type: ActionTypes.SET_RECEIVED_NODES_DELTA }), 0);

// TODO: This way of getting the Scope state is a bit hacky, so try to replace
// it with something better. The problem is that all the actions that are called
// from the components wrapped in <CloudFeature /> have a global Service state
// returned by getState(). Since method is called from both contexts, getState()
// will sometimes return Scope state subtree and sometimes the whole Service state.
const state = getState().scope || getState();
const movingInTime = state.get('timeTravelTransitioning');
const hasChanges = delta.add || delta.update || delta.remove;

if (hasChanges || movingInTime) {
if (isPausedSelector(state)) {
if (state.get('nodesDeltaBuffer').size >= NODES_DELTA_BUFFER_SIZE_LIMIT) {
dispatch({ type: ActionTypes.CONSOLIDATE_NODES_DELTA_BUFFER });
}
dispatch({ type: ActionTypes.BUFFER_NODES_DELTA, delta });
} else {
if (!isPausedSelector(getState())) {
// 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.
setTimeout(() => dispatch({ type: ActionTypes.SET_RECEIVED_NODES_DELTA }), 0);

// When moving in time, we will consider the transition complete
// 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')) {
dispatch({ type: ActionTypes.FINISH_TIME_TRAVEL_TRANSITION });
}

const hasChanges = delta.add || delta.update || delta.remove;
if (hasChanges) {
dispatch({
type: ActionTypes.RECEIVE_NODES_DELTA,
delta
Expand All @@ -605,30 +562,58 @@ export function receiveNodesDelta(delta) {
};
}

function updateFromNodesDeltaBuffer(dispatch, state) {
const isPaused = isPausedSelector(state);
const isBufferEmpty = state.get('nodesDeltaBuffer').isEmpty();
export function resumeTime() {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.RESUME_TIME
});
// After unpausing, all of the following calls will re-activate polling.
getTopologies(getState, dispatch);
getNodes(getState, dispatch, true);
if (isResourceViewModeSelector(getState())) {
getResourceViewNodesSnapshot(getState(), dispatch);
}
};
}

export function startTimeTravel() {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.START_TIME_TRAVEL
});
if (!getState().get('nodesLoaded')) {
getNodes(getState, dispatch);
if (isResourceViewModeSelector(getState())) {
getResourceViewNodesSnapshot(getState(), dispatch);
}
}
};
}

export function receiveNodes(nodes) {
return {
type: ActionTypes.RECEIVE_NODES,
nodes,
};
}

if (isPaused || isBufferEmpty) {
dispatch(resetNodesDeltaBuffer());
} else {
dispatch(receiveNodesDelta(state.get('nodesDeltaBuffer').first()));
dispatch({ type: ActionTypes.POP_NODES_DELTA_BUFFER });
}
export function timeTravelStartTransition() {
return {
type: ActionTypes.TIME_TRAVEL_START_TRANSITION,
};
}

export function clickResumeUpdate() {
export function jumpToTime(timestamp) {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.CLICK_RESUME_UPDATE
type: ActionTypes.JUMP_TO_TIME,
timestamp,
});
// TODO: Find a better way to do this (see the comment above).
const state = getState().scope || getState();
// Periodically merge buffered nodes deltas until the buffer is emptied.
nodesDeltaBufferUpdateTimer = setInterval(
() => updateFromNodesDeltaBuffer(dispatch, state),
NODES_DELTA_BUFFER_FEED_INTERVAL,
);
getTopologies(getState, dispatch);
getNodes(getState, dispatch);
if (isResourceViewModeSelector(getState())) {
getResourceViewNodesSnapshot(getState(), dispatch);
}
};
}

Expand All @@ -641,18 +626,15 @@ export function receiveNodesForTopology(nodes, topologyId) {
}

export function receiveTopologies(topologies) {
return (dispatch, getGlobalState) => {
// NOTE: Fortunately, this will go when Time Travel is out of <CloudFeature />.
const getState = () => getGlobalState().scope || getGlobalState();
return (dispatch, getState) => {
const firstLoad = !getState().get('topologiesLoaded');
dispatch({
type: ActionTypes.RECEIVE_TOPOLOGIES,
topologies
});
const state = getState();
updateWebsocketChannel(state, dispatch);
getNodeDetails(state, dispatch);
getNodes(getState, dispatch);
// Populate search matches on first load
const state = getState();
if (firstLoad && state.get('searchQuery')) {
dispatch(focusSearch());
}
Expand Down Expand Up @@ -754,7 +736,7 @@ export function setContrastMode(enabled) {

export function getTopologiesWithInitialPoll() {
return (dispatch, getState) => {
getTopologies(getState(), dispatch, true);
getTopologies(getState, dispatch, true);
};
}

Expand All @@ -765,13 +747,12 @@ export function route(urlState) {
type: ActionTypes.ROUTE_TOPOLOGY
});
// update all request workers with new options
const state = getState();
getTopologies(state, dispatch);
updateWebsocketChannel(state, dispatch);
getNodeDetails(state, dispatch);
getTopologies(getState, dispatch);
getNodes(getState, dispatch);
// If we are landing on the resource view page, we need to fetch not only all the
// nodes for the current topology, but also the nodes of all the topologies that make
// the layers in the resource view.
const state = getState();
if (isResourceViewModeSelector(state)) {
getResourceViewNodesSnapshot(state, dispatch);
}
Expand Down
35 changes: 20 additions & 15 deletions client/app/scripts/components/app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import debug from 'debug';
import React from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { debounce } from 'lodash';

Expand All @@ -12,7 +13,6 @@ import Search from './search';
import Status from './status';
import Topologies from './topologies';
import TopologyOptions from './topology-options';
import CloudFeature from './cloud-feature';
import Overlay from './overlay';
import { getApiDetails } from '../utils/web-api-utils';
import {
Expand All @@ -33,6 +33,7 @@ import {
import Details from './details';
import Nodes from './nodes';
import TimeTravel from './time-travel';
import TimeControl from './time-control';
import ViewModeSelector from './view-mode-selector';
import NetworkSelector from './networks-selector';
import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar';
Expand Down Expand Up @@ -166,12 +167,15 @@ class App extends React.Component {
}

render() {
const { isTableViewMode, isGraphViewMode, isResourceViewMode, showingDetails, showingHelp,
showingNetworkSelector, showingTroubleshootingMenu, timeTravelTransitioning } = this.props;
const { isTableViewMode, isGraphViewMode, isResourceViewMode, showingDetails,
showingHelp, showingNetworkSelector, showingTroubleshootingMenu,
timeTravelTransitioning, showingTimeTravel } = this.props;

const className = classNames('scope-app', { 'time-travel-open': showingTimeTravel });
const isIframe = window !== window.top;

return (
<div className="scope-app" ref={this.saveAppRef}>
<div className={className} ref={this.saveAppRef}>
{showingDebugToolbar() && <DebugToolbar />}

{showingHelp && <HelpPanel />}
Expand All @@ -181,22 +185,22 @@ class App extends React.Component {
{showingDetails && <Details />}

<div className="header">
<div className="logo">
{!isIframe && <svg width="100%" height="100%" viewBox="0 0 1089 217">
<Logo />
</svg>}
<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 />
</div>

<Nodes />

<CloudFeature>
{!isResourceViewMode && <TimeTravel />}
</CloudFeature>

<Sidebar classNames={isTableViewMode ? 'sidebar-gridmode' : ''}>
{showingNetworkSelector && isGraphViewMode && <NetworkSelector />}
{!isResourceViewMode && <Status />}
Expand Down Expand Up @@ -224,6 +228,7 @@ 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,
Expand Down
2 changes: 0 additions & 2 deletions client/app/scripts/components/footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import { connect } from 'react-redux';

import Plugins from './plugins';
import PauseButton from './pause-button';
import { trackMixpanelEvent } from '../utils/tracking-utils';
import {
clickDownloadGraph,
Expand Down Expand Up @@ -66,7 +65,6 @@ class Footer extends React.Component {
</div>

<div className="footer-tools">
<PauseButton />
<a
className="footer-icon"
onClick={this.handleRelayoutClick}
Expand Down
Loading