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

Make API calls with time travel timestamp #2600

Merged
merged 11 commits into from
Jun 20, 2017
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