From eb64d3f09bb68b40ec3497d4581bbdf6d6aa6d87 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 20 Jun 2017 12:31:22 +0200 Subject: [PATCH] Make API calls with time travel timestamp (#2600) * 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. --- app/api_topologies.go | 23 +++++- app/api_topology.go | 18 ++--- client/app/scripts/actions/app-actions.js | 81 +++++++------------ client/app/scripts/components/app.js | 13 +-- .../node-details/node-details-info.js | 16 +++- .../node-details/node-details-table-row.js | 8 +- .../node-details/node-details-table.js | 19 ++++- client/app/scripts/components/nodes.js | 12 ++- client/app/scripts/components/overlay.js | 11 +++ client/app/scripts/components/status.js | 12 +-- client/app/scripts/components/time-travel.js | 22 ++--- client/app/scripts/constants/action-types.js | 4 +- .../scripts/reducers/__tests__/root-test.js | 2 +- client/app/scripts/reducers/root.js | 30 ++++--- client/app/scripts/selectors/node-filters.js | 37 +++++++++ client/app/scripts/selectors/time-travel.js | 7 +- client/app/scripts/selectors/topology.js | 3 +- .../utils/__tests__/web-api-utils-test.js | 27 ++++++- client/app/scripts/utils/string-utils.js | 14 ++-- client/app/scripts/utils/topology-utils.js | 10 ++- client/app/scripts/utils/web-api-utils.js | 76 ++++++++--------- client/app/styles/_base.scss | 23 ++++-- client/package.json | 1 + client/yarn.lock | 8 +- 24 files changed, 291 insertions(+), 186 deletions(-) create mode 100644 client/app/scripts/components/overlay.js diff --git a/app/api_topologies.go b/app/api_topologies.go index e2e7793608..bb16836fe9 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -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...) @@ -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 @@ -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 diff --git a/app/api_topology.go b/app/api_topology.go index e0c8e2f5a5..173e275b99 100644 --- a/app/api_topology.go +++ b/app/api_topology.go @@ -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) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 359ea5fbb4..a06479960e 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -25,7 +25,6 @@ import { pinnedMetricSelector, } from '../selectors/node-metric'; import { - activeTopologyOptionsSelector, isResourceViewModeSelector, resourceViewAvailableSelector, } from '../selectors/topology'; @@ -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); }; } @@ -336,7 +329,7 @@ export function setResourceView() { const firstAvailableMetricType = availableMetricTypesSelector(state).first(); dispatch(pinMetric(firstAvailableMetricType)); } - getResourceViewNodesSnapshot(getState, dispatch); + getResourceViewNodesSnapshot(getState(), dispatch); updateRoute(getState); } }; @@ -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); }; } @@ -378,14 +364,7 @@ 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); }; } @@ -393,7 +372,7 @@ 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 @@ -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); + } }; } @@ -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); }; } @@ -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) { @@ -658,7 +641,9 @@ 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 . + const getState = () => getGlobalState().scope || getGlobalState(); const firstLoad = !getState().get('topologiesLoaded'); dispatch({ type: ActionTypes.RECEIVE_TOPOLOGIES, @@ -666,20 +651,14 @@ export function receiveTopologies(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); } }; } @@ -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({ @@ -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); } }; } diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 6bd8ab7cc9..1b01062cc2 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -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, @@ -27,6 +28,7 @@ import { setResourceView, shutdown, setViewportDimensions, + getTopologiesWithInitialPoll, } from '../actions/app-actions'; import Details from './details'; import Nodes from './nodes'; @@ -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, @@ -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); } @@ -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 ( @@ -203,6 +204,8 @@ class App extends React.Component {