@@ -61,3 +63,11 @@ export default class NodeDetailsInfo extends React.Component {
);
}
}
+
+function mapStateToProps(state) {
+ return {
+ timestamp: getSerializedTimeTravelTimestamp(state),
+ };
+}
+
+export default connect(mapStateToProps)(NodeDetailsInfo);
diff --git a/client/app/scripts/components/node-details/node-details-table-row.js b/client/app/scripts/components/node-details/node-details-table-row.js
index d1ad47b058..5b48831abd 100644
--- a/client/app/scripts/components/node-details/node-details-table-row.js
+++ b/client/app/scripts/components/node-details/node-details-table-row.js
@@ -30,14 +30,14 @@ function getValuesForNode(node) {
return values;
}
-function renderValues(node, columns = [], columnStyles = []) {
+function renderValues(node, columns = [], columnStyles = [], timestamp = null) {
const fields = getValuesForNode(node);
return columns.map(({id}, i) => {
const field = fields[id];
const style = columnStyles[i];
if (field) {
if (field.valueType === 'metadata') {
- const {value, title} = formatDataType(field);
+ const { value, title } = formatDataType(field, timestamp);
return (
h.id === sortedBy);
@@ -265,6 +267,7 @@ export default class NodeDetailsTable extends React.Component {
onClick={onClickRow}
onMouseEnter={this.onMouseEnterRow}
onMouseLeave={this.onMouseLeaveRow}
+ timestamp={timestamp}
topologyId={topologyId} />
))}
{minHeightConstraint(tableContentMinHeightConstraint)}
@@ -288,3 +291,11 @@ NodeDetailsTable.defaultProps = {
sortedDesc: null,
sortedBy: null,
};
+
+function mapStateToProps(state) {
+ return {
+ timestamp: getSerializedTimeTravelTimestamp(state),
+ };
+}
+
+export default connect(mapStateToProps)(NodeDetailsTable);
diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js
index fdae6bf2b9..78b4945345 100644
--- a/client/app/scripts/components/nodes.js
+++ b/client/app/scripts/components/nodes.js
@@ -1,5 +1,4 @@
import React from 'react';
-import classNames from 'classnames';
import { connect } from 'react-redux';
import NodesChart from '../charts/nodes-chart';
@@ -12,6 +11,7 @@ import {
isTopologyNodeCountZero,
isNodesDisplayEmpty,
} from '../utils/topology-utils';
+import { nodesLoadedSelector } from '../selectors/node-filters';
import {
isGraphViewModeSelector,
isTableViewModeSelector,
@@ -53,13 +53,11 @@ class Nodes extends React.Component {
render() {
const { topologiesLoaded, nodesLoaded, topologies, currentTopology, isGraphViewMode,
- isTableViewMode, isResourceViewMode, websocketTransitioning } = this.props;
-
- const className = classNames('nodes-wrapper', { blurred: websocketTransitioning });
+ isTableViewMode, isResourceViewMode } = this.props;
// TODO: Rename view mode components.
return (
-
+
;
+ }
+}
diff --git a/client/app/scripts/components/status.js b/client/app/scripts/components/status.js
index cb0c0861d3..4332b7f775 100644
--- a/client/app/scripts/components/status.js
+++ b/client/app/scripts/components/status.js
@@ -1,13 +1,12 @@
import React from 'react';
import { connect } from 'react-redux';
-import { isWebsocketQueryingCurrentSelector } from '../selectors/time-travel';
+import { isNowSelector } from '../selectors/time-travel';
class Status extends React.Component {
render() {
- const { errorUrl, topologiesLoaded, filteredNodeCount, topology,
- websocketClosed, showingCurrentState } = this.props;
+ const { errorUrl, topologiesLoaded, filteredNodeCount, topology, websocketClosed } = this.props;
let title = '';
let text = 'Trying to reconnect...';
@@ -33,11 +32,6 @@ class Status extends React.Component {
}
classNames += ' status-stats';
showWarningIcon = false;
- // TODO: Currently the stats are always pulled for the current state of the system,
- // so they are incorrect when showing the past. This should be addressed somehow.
- if (!showingCurrentState) {
- text = '';
- }
}
return (
@@ -53,7 +47,7 @@ function mapStateToProps(state) {
return {
errorUrl: state.get('errorUrl'),
filteredNodeCount: state.get('nodes').filter(node => node.get('filtered')).size,
- showingCurrentState: isWebsocketQueryingCurrentSelector(state),
+ showingCurrentState: isNowSelector(state),
topologiesLoaded: state.get('topologiesLoaded'),
topology: state.get('currentTopology'),
websocketClosed: state.get('websocketClosed'),
diff --git a/client/app/scripts/components/time-travel.js b/client/app/scripts/components/time-travel.js
index 3d70a0afe0..282e7dc634 100644
--- a/client/app/scripts/components/time-travel.js
+++ b/client/app/scripts/components/time-travel.js
@@ -8,8 +8,8 @@ import { debounce } from 'lodash';
import TimeTravelTimestamp from './time-travel-timestamp';
import { trackMixpanelEvent } from '../utils/tracking-utils';
import {
- websocketQueryInPast,
- startWebsocketTransitionLoader,
+ timeTravelJumpToPast,
+ timeTravelStartTransition,
clickResumeUpdate,
} from '../actions/app-actions';
@@ -112,7 +112,7 @@ class TimeTravel extends React.Component {
}
this.setState({ millisecondsInPast });
- this.props.startWebsocketTransitionLoader();
+ this.props.timeTravelStartTransition();
this.debouncedUpdateTimestamp(millisecondsInPast);
this.debouncedTrackSliderChange();
@@ -125,7 +125,7 @@ class TimeTravel extends React.Component {
if (this.state.millisecondsInPast > rangeMilliseconds) {
this.setState({ millisecondsInPast: rangeMilliseconds });
this.updateTimestamp(rangeMilliseconds);
- this.props.startWebsocketTransitionLoader();
+ this.props.timeTravelStartTransition();
}
trackMixpanelEvent('scope.time.range.select', {
@@ -143,7 +143,7 @@ class TimeTravel extends React.Component {
rangeOptionSelected: sliderRanges.last1Hour,
});
this.updateTimestamp();
- this.props.startWebsocketTransitionLoader();
+ this.props.timeTravelStartTransition();
trackMixpanelEvent('scope.time.now.click', {
layout: this.props.topologyViewMode,
@@ -165,7 +165,7 @@ class TimeTravel extends React.Component {
}
updateTimestamp(millisecondsInPast = 0) {
- this.props.websocketQueryInPast(millisecondsInPast);
+ this.props.timeTravelJumpToPast(millisecondsInPast);
this.props.clickResumeUpdate();
}
@@ -215,7 +215,7 @@ class TimeTravel extends React.Component {
}
render() {
- const { websocketTransitioning, hasTimeTravel } = this.props;
+ const { timeTravelTransitioning, hasTimeTravel } = this.props;
const { showSliderPanel, millisecondsInPast, rangeOptionSelected } = this.state;
const lowerCaseLabel = rangeOptionSelected.label.toLowerCase();
const isCurrent = (millisecondsInPast === 0);
@@ -250,7 +250,7 @@ class TimeTravel extends React.Component {
{this.renderTimeSlider()}
}
- {websocketTransitioning &&
+ {timeTravelTransitioning &&
}
{
// other topology w/o options dont return options, but keep in app state
nextState = reducer(nextState, ClickTopology2Action);
- expect(activeTopologyOptionsSelector(nextState)).toNotExist();
+ expect(activeTopologyOptionsSelector(nextState).size).toEqual(0);
expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['off']);
});
diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js
index d8f345a3d9..f1f2a164ce 100644
--- a/client/app/scripts/reducers/root.js
+++ b/client/app/scripts/reducers/root.js
@@ -76,6 +76,8 @@ export const initialState = makeMap({
showingHelp: false,
showingTroubleshootingMenu: false,
showingNetworks: false,
+ timeTravelTransitioning: false,
+ timeTravelMillisecondsInPast: 0,
topologies: makeList(),
topologiesLoaded: false,
topologyOptions: makeOrderedMap(), // topologyId -> options
@@ -86,8 +88,6 @@ export const initialState = makeMap({
versionUpdate: null,
viewport: makeMap(),
websocketClosed: false,
- websocketTransitioning: false,
- websocketQueryMillisecondsInPast: 0,
zoomCache: makeMap(),
serviceImages: makeMap()
});
@@ -124,7 +124,7 @@ function processTopologies(state, nextTopologies) {
const topologiesWithFullnames = addTopologyFullname(topologiesWithId);
const immNextTopologies = fromJS(topologiesWithFullnames).sortBy(topologySorter);
- return state.mergeDeepIn(['topologies'], immNextTopologies);
+ return state.set('topologies', immNextTopologies);
}
function setTopology(state, topologyId) {
@@ -290,7 +290,7 @@ export function rootReducer(state = initialState, action) {
}
case ActionTypes.CLICK_PAUSE_UPDATE: {
- const millisecondsInPast = state.get('websocketQueryMillisecondsInPast');
+ const millisecondsInPast = state.get('timeTravelMillisecondsInPast');
return state.set('updatePausedAt', moment().utc().subtract(millisecondsInPast));
}
@@ -347,19 +347,23 @@ export function rootReducer(state = initialState, action) {
}
//
- // websockets
+ // time travel
//
- case ActionTypes.OPEN_WEBSOCKET: {
- return state.set('websocketClosed', false);
+ case ActionTypes.TIME_TRAVEL_START_TRANSITION: {
+ return state.set('timeTravelTransitioning', true);
}
- case ActionTypes.START_WEBSOCKET_TRANSITION_LOADER: {
- return state.set('websocketTransitioning', true);
+ case ActionTypes.TIME_TRAVEL_MILLISECONDS_IN_PAST: {
+ return state.set('timeTravelMillisecondsInPast', action.millisecondsInPast);
}
- case ActionTypes.WEBSOCKET_QUERY_MILLISECONDS_IN_PAST: {
- return state.set('websocketQueryMillisecondsInPast', action.millisecondsInPast);
+ //
+ // websockets
+ //
+
+ case ActionTypes.OPEN_WEBSOCKET: {
+ return state.set('websocketClosed', false);
}
case ActionTypes.CLOSE_WEBSOCKET: {
@@ -569,8 +573,8 @@ export function rootReducer(state = initialState, action) {
// 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 (state.get('websocketTransitioning')) {
- state = state.set('websocketTransitioning', false);
+ if (state.get('timeTravelTransitioning')) {
+ state = state.set('timeTravelTransitioning', false);
state = clearNodes(state);
}
diff --git a/client/app/scripts/selectors/node-filters.js b/client/app/scripts/selectors/node-filters.js
index 0d1d0c7d3b..b8b517ecf9 100644
--- a/client/app/scripts/selectors/node-filters.js
+++ b/client/app/scripts/selectors/node-filters.js
@@ -1,4 +1,9 @@
import { createSelector } from 'reselect';
+import { fromJS } from 'immutable';
+
+import { isResourceViewModeSelector } from './topology';
+import { layoutNodesByTopologyIdSelector } from './resource-view/layout';
+import { RESOURCE_VIEW_LAYERS } from '../constants/resources';
export const shownNodesSelector = createSelector(
@@ -7,3 +12,35 @@ export const shownNodesSelector = createSelector(
],
nodes => nodes.filter(node => !node.get('filtered'))
);
+
+export const shownResourceTopologyIdsSelector = createSelector(
+ [
+ layoutNodesByTopologyIdSelector,
+ ],
+ layoutNodesByTopologyId => layoutNodesByTopologyId.filter(nodes => !nodes.isEmpty()).keySeq()
+);
+
+// TODO: Get rid of this logic by unifying `nodes` and `nodesByTopology` global states.
+const loadedAllResourceLayersSelector = createSelector(
+ [
+ state => state.get('nodesByTopology').keySeq(),
+ ],
+ resourceViewLoadedTopologyIds => fromJS(RESOURCE_VIEW_LAYERS).keySeq()
+ .every(topId => resourceViewLoadedTopologyIds.contains(topId))
+);
+
+export const nodesLoadedSelector = createSelector(
+ [
+ state => state.get('nodesLoaded'),
+ loadedAllResourceLayersSelector,
+ isResourceViewModeSelector,
+ ],
+ (nodesLoaded, loadedAllResourceLayers, isResourceViewMode) => (
+ // Since `nodesLoaded` is set when we receive nodes delta over websockets,
+ // it's a completely wrong criterion for determining if resource view is
+ // in the loading state - instead we look at the 'static' topologies whose
+ // nodes were loaded into 'nodesByTopology' and say resource view has been
+ // loaded if nodes for all the resource layer topologies have been loaded once.
+ isResourceViewMode ? loadedAllResourceLayers : nodesLoaded
+ )
+);
diff --git a/client/app/scripts/selectors/time-travel.js b/client/app/scripts/selectors/time-travel.js
index ae84cb43d8..141543697a 100644
--- a/client/app/scripts/selectors/time-travel.js
+++ b/client/app/scripts/selectors/time-travel.js
@@ -8,9 +8,10 @@ export const isPausedSelector = createSelector(
updatePausedAt => updatePausedAt !== null
);
-export const isWebsocketQueryingCurrentSelector = createSelector(
+export const isNowSelector = createSelector(
[
- state => state.get('websocketQueryMillisecondsInPast')
+ state => state.get('timeTravelMillisecondsInPast')
],
- websocketQueryMillisecondsInPast => websocketQueryMillisecondsInPast === 0
+ // true for values 0, undefined, null, etc...
+ timeTravelMillisecondsInPast => !(timeTravelMillisecondsInPast > 0)
);
diff --git a/client/app/scripts/selectors/topology.js b/client/app/scripts/selectors/topology.js
index 90672a1d10..6c9465ff38 100644
--- a/client/app/scripts/selectors/topology.js
+++ b/client/app/scripts/selectors/topology.js
@@ -1,4 +1,5 @@
import { createSelector } from 'reselect';
+import { Map as makeMap } from 'immutable';
import { layersTopologyIdsSelector } from './resource-view/layout';
import {
@@ -56,6 +57,6 @@ export const activeTopologyOptionsSelector = createSelector(
state => state.get('topologyOptions'),
],
(parentTopologyId, currentTopologyId, topologyOptions) => (
- topologyOptions.get(parentTopologyId || currentTopologyId)
+ topologyOptions.get(parentTopologyId || currentTopologyId, makeMap())
)
);
diff --git a/client/app/scripts/utils/__tests__/web-api-utils-test.js b/client/app/scripts/utils/__tests__/web-api-utils-test.js
index fc467577bd..4b8257bf0b 100644
--- a/client/app/scripts/utils/__tests__/web-api-utils-test.js
+++ b/client/app/scripts/utils/__tests__/web-api-utils-test.js
@@ -1,7 +1,9 @@
+import MockDate from 'mockdate';
+import { Map as makeMap, OrderedMap as makeOrderedMap } from 'immutable';
-import {OrderedMap as makeOrderedMap} from 'immutable';
import { buildUrlQuery, basePath, getApiPath, getWebsocketUrl } from '../web-api-utils';
+
describe('WebApiUtils', () => {
describe('basePath', () => {
it('should handle /scope/terminal.html', () => {
@@ -22,15 +24,34 @@ describe('WebApiUtils', () => {
});
describe('buildUrlQuery', () => {
+ let state = makeMap();
+
+ beforeEach(() => {
+ MockDate.set(1434319925275);
+ });
+
+ afterEach(() => {
+ MockDate.reset();
+ });
+
it('should handle empty options', () => {
- expect(buildUrlQuery(makeOrderedMap({}))).toBe('');
+ expect(buildUrlQuery(makeOrderedMap([]), state)).toBe('');
});
it('should combine multiple options', () => {
+ state = state.set('timeTravelMillisecondsInPast', 0);
+ expect(buildUrlQuery(makeOrderedMap([
+ ['foo', 2],
+ ['bar', 4]
+ ]), state)).toBe('foo=2&bar=4');
+ });
+
+ it('should combine multiple options with a timestamp', () => {
+ state = state.set('timeTravelMillisecondsInPast', 60 * 60 * 1000); // 1h in the past
expect(buildUrlQuery(makeOrderedMap([
['foo', 2],
['bar', 4]
- ]))).toBe('foo=2&bar=4');
+ ]), state)).toBe('foo=2&bar=4×tamp=2015-06-14T21:12:05.275Z');
});
});
diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js
index 25727259f3..e22218996f 100644
--- a/client/app/scripts/utils/string-utils.js
+++ b/client/app/scripts/utils/string-utils.js
@@ -89,18 +89,20 @@ export function ipToPaddedString(value) {
// Formats metadata values. Add a key to the `formatters` obj
// that matches the `dataType` of the field. You must return an Object
// with the keys `value` and `title` defined.
-export function formatDataType(field) {
+// `referenceTimestamp` is the timestamp we've time-travelled to.
+export function formatDataType(field, referenceTimestampStr = null) {
const formatters = {
- datetime(dateString) {
- const date = moment(new Date(dateString));
+ datetime(timestampString) {
+ const timestamp = moment(timestampString);
+ const referenceTimestamp = referenceTimestampStr ? moment(referenceTimestampStr) : moment();
return {
- value: date.fromNow(),
- title: date.format('YYYY-MM-DD HH:mm:ss.SSS')
+ value: timestamp.from(referenceTimestamp),
+ title: timestamp.utc().toISOString()
};
}
};
const format = formatters[field.dataType];
return format
? format(field.value)
- : {value: field.value, title: field.value};
+ : { value: field.value, title: field.value };
}
diff --git a/client/app/scripts/utils/topology-utils.js b/client/app/scripts/utils/topology-utils.js
index e10d9c157d..f92c15088d 100644
--- a/client/app/scripts/utils/topology-utils.js
+++ b/client/app/scripts/utils/topology-utils.js
@@ -1,10 +1,10 @@
import { endsWith } from 'lodash';
import { Set as makeSet, List as makeList } from 'immutable';
-import { isWebsocketQueryingCurrentSelector } from '../selectors/time-travel';
+import { isNowSelector } from '../selectors/time-travel';
import { isResourceViewModeSelector } from '../selectors/topology';
import { pinnedMetricSelector } from '../selectors/node-metric';
-import { shownNodesSelector } from '../selectors/node-filters';
+import { shownNodesSelector, shownResourceTopologyIdsSelector } from '../selectors/node-filters';
//
// top priority first
@@ -137,13 +137,15 @@ export function getCurrentTopologyOptions(state) {
export function isTopologyNodeCountZero(state) {
const nodeCount = state.getIn(['currentTopology', 'stats', 'node_count'], 0);
- return nodeCount === 0 && isWebsocketQueryingCurrentSelector(state);
+ // If we are browsing the past, assume there would normally be some nodes at different times.
+ // If we are in the resource view, don't rely on these stats at all (for now).
+ return nodeCount === 0 && isNowSelector(state) && !isResourceViewModeSelector(state);
}
export function isNodesDisplayEmpty(state) {
// Consider a topology in the resource view empty if it has no pinned metric.
if (isResourceViewModeSelector(state)) {
- return !pinnedMetricSelector(state);
+ return !pinnedMetricSelector(state) || shownResourceTopologyIdsSelector(state).isEmpty();
}
// Otherwise (in graph and table view), we only look at the nodes content.
return shownNodesSelector(state).isEmpty();
diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js
index be177efac1..950b5af4e7 100644
--- a/client/app/scripts/utils/web-api-utils.js
+++ b/client/app/scripts/utils/web-api-utils.js
@@ -2,7 +2,7 @@ import debug from 'debug';
import moment from 'moment';
import reqwest from 'reqwest';
import { defaults } from 'lodash';
-import { fromJS, Map as makeMap, List } from 'immutable';
+import { Map as makeMap, List } from 'immutable';
import { blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveError,
receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError,
@@ -13,7 +13,8 @@ import { blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveEr
import { getCurrentTopologyUrl } from '../utils/topology-utils';
import { layersTopologyIdsSelector } from '../selectors/resource-view/layout';
import { activeTopologyOptionsSelector } from '../selectors/topology';
-import { isWebsocketQueryingCurrentSelector } from '../selectors/time-travel';
+import { isNowSelector } from '../selectors/time-travel';
+
import { API_REFRESH_INTERVAL, TOPOLOGY_REFRESH_INTERVAL } from '../constants/timer';
const log = debug('scope:web-api-utils');
@@ -46,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) => {
@@ -97,13 +108,10 @@ 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) {
- const query = buildUrlQuery(fromJS({
- t: updateFrequency,
- timestamp: queryTimestamp,
- ...topologyOptions.toJS(),
- }));
- return `${getWebsocketUrl()}${topologyUrl}/ws?${query}`;
+function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), state) {
+ topologyOptions = topologyOptions.set('t', updateFrequency);
+ const optionsQuery = buildUrlQuery(topologyOptions, state);
+ return `${getWebsocketUrl()}${topologyUrl}/ws?${optionsQuery}`;
}
function createWebsocket(websocketUrl, dispatch) {
@@ -179,12 +187,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))),
@@ -194,27 +202,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 getResourceViewNodesSnapshot(state, dispatch) {
+ const topologyIds = layersTopologyIdsSelector(state);
+ getNodesForTopologies(state, dispatch, topologyIds);
}
-export function getTopologies(options, dispatch, initialPoll) {
+export function getTopologies(state, dispatch, initialPoll = false) {
+ // TODO: Remove this once TimeTravel is out of the feature flag.
+ state = state.scope || 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,
@@ -222,7 +231,7 @@ export function getTopologies(options, dispatch, initialPoll) {
if (continuePolling) {
dispatch(receiveTopologies(res));
topologyTimer = setTimeout(() => {
- getTopologies(options, dispatch);
+ getTopologies(state, dispatch);
}, TOPOLOGY_REFRESH_INTERVAL);
}
},
@@ -232,26 +241,17 @@ export function getTopologies(options, dispatch, initialPoll) {
// Only retry in stand-alone mode
if (continuePolling) {
topologyTimer = setTimeout(() => {
- getTopologies(options, dispatch);
+ getTopologies(state, dispatch);
}, TOPOLOGY_REFRESH_INTERVAL);
}
}
});
}
-function getWebsocketQueryTimestamp(state) {
- // The timestamp query parameter will be used only if it's in the past.
- if (isWebsocketQueryingCurrentSelector(state)) return null;
-
- const millisecondsInPast = state.get('websocketQueryMillisecondsInPast');
- return moment().utc().subtract(millisecondsInPast).toISOString();
-}
-
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
@@ -262,7 +262,11 @@ export function updateWebsocketChannel(state, dispatch) {
}
}
-export function getNodeDetails(topologyUrlsById, currentTopologyId, options, nodeMap, dispatch) {
+export function getNodeDetails(state, dispatch) {
+ const nodeMap = state.get('nodeDetails');
+ const topologyUrlsById = state.get('topologyUrlsById');
+ const currentTopologyId = state.get('currentTopologyId');
+
// get details for all opened nodes
const obj = nodeMap.last();
if (obj && topologyUrlsById.has(obj.topologyId)) {
@@ -270,7 +274,7 @@ export function getNodeDetails(topologyUrlsById, currentTopologyId, options, nod
let urlComponents = [getApiPath(), topologyUrl, '/', encodeURIComponent(obj.id)];
if (currentTopologyId === obj.topologyId) {
// Only forward filters for nodes in the current topology
- const optionsQuery = buildUrlQuery(options);
+ const optionsQuery = buildUrlQuery(activeTopologyOptionsSelector(state), state);
urlComponents = urlComponents.concat(['?', optionsQuery]);
}
const url = urlComponents.join('');
diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss
index 0d8654752b..dc304bdb18 100644
--- a/client/app/styles/_base.scss
+++ b/client/app/styles/_base.scss
@@ -51,6 +51,22 @@
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.19), 0 6px 10px rgba(0, 0, 0, 0.23);
}
+.overlay {
+ @extend .hideable;
+
+ background-color: white;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ opacity: 0;
+ pointer-events: none;
+ z-index: 2000;
+
+ &.faded { opacity: 0.5; }
+}
+
.overlay-wrapper {
align-items: center;
background-color: fade-out($background-average-color, 0.1);
@@ -208,16 +224,11 @@
}
}
-.nodes-wrapper {
- @extend .hideable;
-
- &.blurred { opacity: 0.2; }
-}
-
.time-travel {
@extend .overlay-wrapper;
display: block;
right: 530px;
+ z-index: 2001;
&-status {
display: flex;
diff --git a/client/package.json b/client/package.json
index dd9e453153..0d839e1d8e 100644
--- a/client/package.json
+++ b/client/package.json
@@ -75,6 +75,7 @@
"immutable-devtools": "0.0.7",
"jest-cli": "19.0.2",
"json-loader": "0.5.4",
+ "mockdate": "^2.0.1",
"node-sass": "4.5.2",
"postcss-loader": "1.3.3",
"react-addons-perf": "15.4.2",
diff --git a/client/yarn.lock b/client/yarn.lock
index 2368e09d75..8518dc6ffd 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -1678,7 +1678,7 @@ d3-dispatch@1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
-d3-drag@1:
+d3-drag@1, d3-drag@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.0.4.tgz#a9c1609f11dd5530ae275ebd64377ec54efb9d8f"
dependencies:
@@ -1739,7 +1739,7 @@ d3-timer@1:
version "1.0.5"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.5.tgz#b266d476c71b0d269e7ac5f352b410a3b6fe6ef0"
-d3-transition@1:
+d3-transition@1, d3-transition@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.0.4.tgz#e1a9ebae3869a9d9c2874ab00841fa8313ae5de5"
dependencies:
@@ -4070,6 +4070,10 @@ mixin-object@^2.0.1:
dependencies:
minimist "0.0.8"
+mockdate@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-2.0.1.tgz#51bc309e2c4396600d56b6c23a6a0f4182943a36"
+
moment@2.18.1:
version "2.18.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
|