Skip to content

Commit

Permalink
Make API calls with time travel timestamp (#2600)
Browse files Browse the repository at this point in the history
* Node details fetching reports at proper timestamp.

* Corrected all the relevant timestamps in the UI.

* Renamed some state variables.

* Time travel works for topologies list.

* Added a whole screen overlay for time travel.

* Polished the backend.

* Make time travel work also with the Resource View.

* Fixed the jest tests.

* Fixed the empty view message for resource view.

* Some naming polishing.

* Addressed the comments.
  • Loading branch information
fbarl authored Jun 20, 2017
1 parent 5d6c965 commit eb64d3f
Show file tree
Hide file tree
Showing 24 changed files with 291 additions and 186 deletions.
23 changes: 20 additions & 3 deletions app/api_topologies.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,19 @@ type topologyStats struct {
FilteredNodes int `json:"filtered_nodes"`
}

// deserializeTimestamp converts the ISO8601 query param into a proper timestamp.
func deserializeTimestamp(timestamp string) time.Time {
if timestamp != "" {
result, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
log.Errorf("Error parsing timestamp '%s' - make sure the time format is correct", timestamp)
}
return result
}
// Default to current time if no timestamp is provided.
return time.Now()
}

// AddContainerFilters adds to the default Registry (topologyRegistry)'s containerFilters
func AddContainerFilters(newFilters ...APITopologyOption) {
topologyRegistry.AddContainerFilters(newFilters...)
Expand Down Expand Up @@ -477,7 +490,8 @@ func (r *Registry) walk(f func(APITopologyDesc)) {
// makeTopologyList returns a handler that yields an APITopologyList.
func (r *Registry) makeTopologyList(rep Reporter) CtxHandlerFunc {
return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
report, err := rep.Report(ctx, time.Now())
timestamp := deserializeTimestamp(req.URL.Query().Get("timestamp"))
report, err := rep.Report(ctx, timestamp)
if err != nil {
respondWith(w, http.StatusInternalServerError, err)
return
Expand Down Expand Up @@ -564,12 +578,15 @@ type rendererHandler func(context.Context, render.Renderer, render.Decorator, re

func (r *Registry) captureRenderer(rep Reporter, f rendererHandler) CtxHandlerFunc {
return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
topologyID := mux.Vars(req)["topology"]
var (
topologyID = mux.Vars(req)["topology"]
timestamp = deserializeTimestamp(req.URL.Query().Get("timestamp"))
)
if _, ok := r.get(topologyID); !ok {
http.NotFound(w, req)
return
}
rpt, err := rep.Report(ctx, time.Now())
rpt, err := rep.Report(ctx, timestamp)
if err != nil {
respondWith(w, http.StatusInternalServerError, err)
return
Expand Down
18 changes: 6 additions & 12 deletions app/api_topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,14 @@ func handleWebsocket(
}(conn)

var (
previousTopo detailed.NodeSummaries
tick = time.Tick(loop)
wait = make(chan struct{}, 1)
topologyID = mux.Vars(r)["topology"]
channelOpenedAt = time.Now()
// By default we will always be reporting the most recent state.
startReportingAt = time.Now()
previousTopo detailed.NodeSummaries
tick = time.Tick(loop)
wait = make(chan struct{}, 1)
topologyID = mux.Vars(r)["topology"]
startReportingAt = deserializeTimestamp(r.Form.Get("timestamp"))
channelOpenedAt = time.Now()
)

// If the timestamp is provided explicitly by the UI, we start reporting from there.
if timestampStr := r.Form.Get("timestamp"); timestampStr != "" {
startReportingAt, _ = time.Parse(time.RFC3339, timestampStr)
}

rep.WaitOn(ctx, wait)
defer rep.UnWait(ctx, wait)

Expand Down
81 changes: 30 additions & 51 deletions client/app/scripts/actions/app-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
pinnedMetricSelector,
} from '../selectors/node-metric';
import {
activeTopologyOptionsSelector,
isResourceViewModeSelector,
resourceViewAvailableSelector,
} from '../selectors/topology';
Expand Down Expand Up @@ -219,15 +218,9 @@ export function changeTopologyOption(option, value, topologyId, addOrRemove) {
// update all request workers with new options
dispatch(resetNodesDeltaBuffer());
const state = getState();
getTopologies(activeTopologyOptionsSelector(state), dispatch);
getTopologies(state, dispatch);
updateWebsocketChannel(state, dispatch);
getNodeDetails(
state.get('topologyUrlsById'),
state.get('currentTopologyId'),
activeTopologyOptionsSelector(state),
state.get('nodeDetails'),
dispatch
);
getNodeDetails(state, dispatch);
};
}

Expand Down Expand Up @@ -336,7 +329,7 @@ export function setResourceView() {
const firstAvailableMetricType = availableMetricTypesSelector(state).first();
dispatch(pinMetric(firstAvailableMetricType));
}
getResourceViewNodesSnapshot(getState, dispatch);
getResourceViewNodesSnapshot(getState(), dispatch);
updateRoute(getState);
}
};
Expand All @@ -351,14 +344,7 @@ export function clickNode(nodeId, label, origin) {
nodeId
});
updateRoute(getState);
const state = getState();
getNodeDetails(
state.get('topologyUrlsById'),
state.get('currentTopologyId'),
activeTopologyOptionsSelector(state),
state.get('nodeDetails'),
dispatch
);
getNodeDetails(getState(), dispatch);
};
}

Expand All @@ -378,22 +364,15 @@ export function clickRelative(nodeId, topologyId, label, origin) {
topologyId
});
updateRoute(getState);
const state = getState();
getNodeDetails(
state.get('topologyUrlsById'),
state.get('currentTopologyId'),
activeTopologyOptionsSelector(state),
state.get('nodeDetails'),
dispatch
);
getNodeDetails(getState(), dispatch);
};
}

function updateTopology(dispatch, getState) {
const state = getState();
// If we're in the resource view, get the snapshot of all the relevant node topologies.
if (isResourceViewModeSelector(state)) {
getResourceViewNodesSnapshot(getState, dispatch);
getResourceViewNodesSnapshot(state, dispatch);
}
updateRoute(getState);
// update all request workers with new options
Expand Down Expand Up @@ -425,21 +404,25 @@ export function clickTopology(topologyId) {
};
}

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

export function websocketQueryInPast(millisecondsInPast) {
export function timeTravelJumpToPast(millisecondsInPast) {
return (dispatch, getServiceState) => {
dispatch({
type: ActionTypes.WEBSOCKET_QUERY_MILLISECONDS_IN_PAST,
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);
}
};
}

Expand Down Expand Up @@ -504,7 +487,7 @@ export function focusSearch() {
// the nodes delta. The solution would be to implement deeper
// search selectors with per-node caching instead of per-topology.
setTimeout(() => {
getAllNodes(getState, dispatch);
getAllNodes(getState(), dispatch);
}, 1200);
};
}
Expand Down Expand Up @@ -603,7 +586,7 @@ export function receiveNodesDelta(delta) {
// 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('websocketTransitioning');
const movingInTime = state.get('timeTravelTransitioning');
const hasChanges = delta.add || delta.update || delta.remove;

if (hasChanges || movingInTime) {
Expand Down Expand Up @@ -658,28 +641,24 @@ export function receiveNodesForTopology(nodes, topologyId) {
}

export function receiveTopologies(topologies) {
return (dispatch, getState) => {
return (dispatch, getGlobalState) => {
// NOTE: Fortunately, this will go when Time Travel is out of <CloudFeature />.
const getState = () => getGlobalState().scope || getGlobalState();
const firstLoad = !getState().get('topologiesLoaded');
dispatch({
type: ActionTypes.RECEIVE_TOPOLOGIES,
topologies
});
const state = getState();
updateWebsocketChannel(state, dispatch);
getNodeDetails(
state.get('topologyUrlsById'),
state.get('currentTopologyId'),
activeTopologyOptionsSelector(state),
state.get('nodeDetails'),
dispatch
);
getNodeDetails(state, dispatch);
// Populate search matches on first load
if (firstLoad && state.get('searchQuery')) {
dispatch(focusSearch());
}
// Fetch all the relevant nodes once on first load
if (firstLoad && isResourceViewModeSelector(state)) {
getResourceViewNodesSnapshot(getState, dispatch);
getResourceViewNodesSnapshot(state, dispatch);
}
};
}
Expand Down Expand Up @@ -773,6 +752,12 @@ export function setContrastMode(enabled) {
};
}

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

export function route(urlState) {
return (dispatch, getState) => {
dispatch({
Expand All @@ -781,20 +766,14 @@ export function route(urlState) {
});
// update all request workers with new options
const state = getState();
getTopologies(activeTopologyOptionsSelector(state), dispatch);
getTopologies(state, dispatch);
updateWebsocketChannel(state, dispatch);
getNodeDetails(
state.get('topologyUrlsById'),
state.get('currentTopologyId'),
activeTopologyOptionsSelector(state),
state.get('nodeDetails'),
dispatch
);
getNodeDetails(state, 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.
if (isResourceViewModeSelector(state)) {
getResourceViewNodesSnapshot(getState, dispatch);
getResourceViewNodesSnapshot(state, dispatch);
}
};
}
Expand Down
13 changes: 8 additions & 5 deletions client/app/scripts/components/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import Status from './status';
import Topologies from './topologies';
import TopologyOptions from './topology-options';
import CloudFeature from './cloud-feature';
import { getApiDetails, getTopologies } from '../utils/web-api-utils';
import Overlay from './overlay';
import { getApiDetails } from '../utils/web-api-utils';
import {
focusSearch,
pinNextMetric,
Expand All @@ -27,6 +28,7 @@ import {
setResourceView,
shutdown,
setViewportDimensions,
getTopologiesWithInitialPoll,
} from '../actions/app-actions';
import Details from './details';
import Nodes from './nodes';
Expand All @@ -38,7 +40,6 @@ import { getRouter, getUrlState } from '../utils/router-utils';
import { trackMixpanelEvent } from '../utils/tracking-utils';
import { availableNetworksSelector } from '../selectors/node-networks';
import {
activeTopologyOptionsSelector,
isResourceViewModeSelector,
isTableViewModeSelector,
isGraphViewModeSelector,
Expand Down Expand Up @@ -74,7 +75,7 @@ class App extends React.Component {
if (!this.props.routeSet || process.env.WEAVE_CLOUD) {
// dont request topologies when already done via router.
// If running as a component, always request topologies when the app mounts.
getTopologies(this.props.activeTopologyOptions, this.props.dispatch, true);
this.props.dispatch(getTopologiesWithInitialPoll());
}
getApiDetails(this.props.dispatch);
}
Expand Down Expand Up @@ -166,7 +167,7 @@ class App extends React.Component {

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

return (
Expand Down Expand Up @@ -203,6 +204,8 @@ class App extends React.Component {
</Sidebar>

<Footer />

<Overlay faded={timeTravelTransitioning} />
</div>
);
}
Expand All @@ -211,7 +214,6 @@ class App extends React.Component {

function mapStateToProps(state) {
return {
activeTopologyOptions: activeTopologyOptionsSelector(state),
currentTopology: state.get('currentTopology'),
isResourceViewMode: isResourceViewModeSelector(state),
isTableViewMode: isTableViewModeSelector(state),
Expand All @@ -226,6 +228,7 @@ function mapStateToProps(state) {
showingNetworkSelector: availableNetworksSelector(state).count() > 0,
showingTerminal: state.get('controlPipes').size > 0,
topologyViewMode: state.get('topologyViewMode'),
timeTravelTransitioning: state.get('timeTravelTransitioning'),
urlState: getUrlState(state)
};
}
Expand Down
16 changes: 13 additions & 3 deletions client/app/scripts/components/node-details/node-details-info.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React from 'react';
import { connect } from 'react-redux';
import { Map as makeMap } from 'immutable';

import MatchedText from '../matched-text';
import ShowMore from '../show-more';
import { formatDataType } from '../../utils/string-utils';
import { getSerializedTimeTravelTimestamp } from '../../utils/web-api-utils';

export default class NodeDetailsInfo extends React.Component {

class NodeDetailsInfo extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
Expand All @@ -21,7 +23,7 @@ export default class NodeDetailsInfo extends React.Component {
}

render() {
const { matches = makeMap() } = this.props;
const { timestamp, matches = makeMap() } = this.props;
let rows = (this.props.rows || []);
let notShown = 0;

Expand All @@ -39,7 +41,7 @@ export default class NodeDetailsInfo extends React.Component {
return (
<div className="node-details-info">
{rows.map((field) => {
const { value, title } = formatDataType(field);
const { value, title } = formatDataType(field, timestamp);
return (
<div className="node-details-info-field" key={field.id}>
<div className="node-details-info-field-label truncate" title={field.label}>
Expand All @@ -61,3 +63,11 @@ export default class NodeDetailsInfo extends React.Component {
);
}
}

function mapStateToProps(state) {
return {
timestamp: getSerializedTimeTravelTimestamp(state),
};
}

export default connect(mapStateToProps)(NodeDetailsInfo);
Loading

0 comments on commit eb64d3f

Please sign in to comment.