diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index ad89f5b58a..501bc204c8 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -329,7 +329,7 @@ export function setResourceView() { const firstAvailableMetricType = availableMetricTypesSelector(state).first(); dispatch(pinMetric(firstAvailableMetricType)); } - getResourceViewNodesSnapshot(getState, dispatch); + getResourceViewNodesSnapshot(getState(), dispatch); updateRoute(getState); } }; @@ -372,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 @@ -420,6 +420,9 @@ export function timeTravelJumpToPast(millisecondsInPast) { updateWebsocketChannel(scopeState, dispatch); dispatch(resetNodesDeltaBuffer()); getTopologies(getServiceState().scope, dispatch); + if (isResourceViewModeSelector(scopeState)) { + getResourceViewNodesSnapshot(scopeState, dispatch); + } }; } @@ -484,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); }; } @@ -654,7 +657,7 @@ export function receiveTopologies(topologies) { } // Fetch all the relevant nodes once on first load if (firstLoad && isResourceViewModeSelector(state)) { - getResourceViewNodesSnapshot(getState, dispatch); + getResourceViewNodesSnapshot(state, dispatch); } }; } @@ -769,7 +772,7 @@ export function route(urlState) { // 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/node-details/node-details-info.js b/client/app/scripts/components/node-details/node-details-info.js index 215b5d48b9..ad0a888c2a 100644 --- a/client/app/scripts/components/node-details/node-details-info.js +++ b/client/app/scripts/components/node-details/node-details-info.js @@ -5,7 +5,7 @@ import { Map as makeMap } from 'immutable'; import MatchedText from '../matched-text'; import ShowMore from '../show-more'; import { formatDataType } from '../../utils/string-utils'; -import { getWebsocketQueryTimestamp } from '../../utils/web-api-utils'; +import { getSerializedTimeTravelTimestamp } from '../../utils/web-api-utils'; class NodeDetailsInfo extends React.Component { @@ -66,7 +66,7 @@ class NodeDetailsInfo extends React.Component { function mapStateToProps(state) { return { - timestamp: getWebsocketQueryTimestamp(state), + timestamp: getSerializedTimeTravelTimestamp(state), }; } diff --git a/client/app/scripts/components/node-details/node-details-table.js b/client/app/scripts/components/node-details/node-details-table.js index fa8a79c1cd..ab752c0f11 100644 --- a/client/app/scripts/components/node-details/node-details-table.js +++ b/client/app/scripts/components/node-details/node-details-table.js @@ -11,7 +11,7 @@ import NodeDetailsTableRow from './node-details-table-row'; import NodeDetailsTableHeaders from './node-details-table-headers'; import { ipToPaddedString } from '../../utils/string-utils'; import { moveElement, insertElement } from '../../utils/array-utils'; -import { getWebsocketQueryTimestamp } from '../../utils/web-api-utils'; +import { getSerializedTimeTravelTimestamp } from '../../utils/web-api-utils'; import { isIP, isNumber, defaultSortDesc, getTableColumnsStyles } from '../../utils/node-details-utils'; @@ -293,7 +293,7 @@ NodeDetailsTable.defaultProps = { function mapStateToProps(state) { return { - timestamp: getWebsocketQueryTimestamp(state), + timestamp: getSerializedTimeTravelTimestamp(state), }; } diff --git a/client/app/scripts/utils/topology-utils.js b/client/app/scripts/utils/topology-utils.js index 1b06c6d53e..d1103d6f16 100644 --- a/client/app/scripts/utils/topology-utils.js +++ b/client/app/scripts/utils/topology-utils.js @@ -143,6 +143,7 @@ export function isTopologyNodeCountZero(state) { export function isNodesDisplayEmpty(state) { // Consider a topology in the resource view empty if it has no pinned metric. if (isResourceViewModeSelector(state)) { + // TODO: Check for empty displays here after Scope v1.5.0 has been released. return !pinnedMetricSelector(state); } // Otherwise (in graph and table view), we only look at the nodes content. diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index d81867668e..9b1f65506f 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -47,8 +47,18 @@ let createWebsocketAt = null; let firstMessageOnWebsocketAt = null; let continuePolling = true; -export function buildUrlQuery(params) { - if (!params) return ''; + +export function getSerializedTimeTravelTimestamp(state) { + // The timestamp parameter will be used only if it's in the past. + if (isNowSelector(state)) return null; + + const millisecondsInPast = state.get('timeTravelMillisecondsInPast'); + return moment().utc().subtract(millisecondsInPast).toISOString(); +} + +export function buildUrlQuery(params = makeMap(), state) { + // Attach the time travel timestamp to every request to the backend. + params = params.set('timestamp', getSerializedTimeTravelTimestamp(state)); // Ignore the entries with values `null` or `undefined`. return params.map((value, param) => { @@ -98,12 +108,11 @@ export function getWebsocketUrl(host = window.location.host, pathname = window.l return `${wsProto}://${host}${process.env.SCOPE_API_PREFIX || ''}${basePath(pathname)}`; } -function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), queryTimestamp) { +function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), state) { const query = buildUrlQuery(fromJS({ t: updateFrequency, - timestamp: queryTimestamp, ...topologyOptions.toJS(), - })); + }), state); return `${getWebsocketUrl()}${topologyUrl}/ws?${query}`; } @@ -180,12 +189,12 @@ function doRequest(opts) { /** * Does a one-time fetch of all the nodes for a custom list of topologies. */ -function getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions = makeMap()) { +function getNodesForTopologies(state, dispatch, topologyIds, topologyOptions = makeMap()) { // fetch sequentially - getState().get('topologyUrlsById') + state.get('topologyUrlsById') .filter((_, topologyId) => topologyIds.contains(topologyId)) .reduce((sequence, topologyUrl, topologyId) => sequence.then(() => { - const optionsQuery = buildUrlQuery(topologyOptions.get(topologyId)); + const optionsQuery = buildUrlQuery(topologyOptions.get(topologyId), state); return doRequest({ url: `${getApiPath()}${topologyUrl}?${optionsQuery}` }); }) .then(json => dispatch(receiveNodesForTopology(json.nodes, topologyId))), @@ -195,39 +204,28 @@ function getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions /** * Gets nodes for all topologies (for search). */ -export function getAllNodes(getState, dispatch) { - const state = getState(); +export function getAllNodes(state, dispatch) { const topologyOptions = state.get('topologyOptions'); const topologyIds = state.get('topologyUrlsById').keySeq(); - getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions); + getNodesForTopologies(state, dispatch, topologyIds, topologyOptions); } /** * One-time update of all the nodes of topologies that appear in the current resource view. * TODO: Replace the one-time snapshot with periodic polling. */ -export function getResourceViewNodesSnapshot(getState, dispatch) { - const topologyIds = layersTopologyIdsSelector(getState()); - getNodesForTopologies(getState, dispatch, topologyIds); -} - -export function getWebsocketQueryTimestamp(state) { - // The timestamp query parameter will be used only if it's in the past. - if (isNowSelector(state)) return null; - - const millisecondsInPast = state.get('timeTravelMillisecondsInPast'); - return moment().utc().subtract(millisecondsInPast).toISOString(); +export function getResourceViewNodesSnapshot(state, dispatch) { + const topologyIds = layersTopologyIdsSelector(state); + getNodesForTopologies(state, dispatch, topologyIds); } export function getTopologies(state, dispatch, initialPoll = false) { + // TODO: Remove this once TimeTravel is out of the feature flag. state = state.scope || state; - let options = activeTopologyOptionsSelector(state); - options = options.set('timestamp', getWebsocketQueryTimestamp(state)); - // Used to resume polling when navigating between pages in Weave Cloud. continuePolling = initialPoll === true ? true : continuePolling; clearTimeout(topologyTimer); - const optionsQuery = buildUrlQuery(options); + const optionsQuery = buildUrlQuery(activeTopologyOptionsSelector(state), state); const url = `${getApiPath()}/api/topology?${optionsQuery}`; doRequest({ url, @@ -255,8 +253,7 @@ export function getTopologies(state, dispatch, initialPoll = false) { export function updateWebsocketChannel(state, dispatch) { const topologyUrl = getCurrentTopologyUrl(state); const topologyOptions = activeTopologyOptionsSelector(state); - const queryTimestamp = getWebsocketQueryTimestamp(state); - const websocketUrl = buildWebsocketUrl(topologyUrl, topologyOptions, queryTimestamp); + const websocketUrl = buildWebsocketUrl(topologyUrl, topologyOptions, state); // Only recreate websocket if url changed or if forced (weave cloud instance reload); const isNewUrl = websocketUrl !== currentUrl; // `topologyUrl` can be undefined initially, so only create a socket if it is truthy @@ -271,7 +268,7 @@ export function getNodeDetails(state, dispatch) { const nodeMap = state.get('nodeDetails'); const topologyUrlsById = state.get('topologyUrlsById'); const currentTopologyId = state.get('currentTopologyId'); - let options = activeTopologyOptionsSelector(state); + const options = activeTopologyOptionsSelector(state); // get details for all opened nodes const obj = nodeMap.last(); @@ -280,8 +277,7 @@ export function getNodeDetails(state, dispatch) { let urlComponents = [getApiPath(), topologyUrl, '/', encodeURIComponent(obj.id)]; if (currentTopologyId === obj.topologyId) { // Only forward filters for nodes in the current topology - options = options.set('timestamp', getWebsocketQueryTimestamp(state)); - const optionsQuery = buildUrlQuery(options); + const optionsQuery = buildUrlQuery(options, state); urlComponents = urlComponents.concat(['?', optionsQuery]); } const url = urlComponents.join('');