mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 17:51:21 +00:00
Time travel redesign (#2651)
* Initial top level control. * Added the jump buttons. * Tiny styling adjustments. * Massive renaming. * Pause info * Added slider marks. * Improved messaging. * Freeze all updates when paused. * Repositioned for Configure button. * Improved the flow. * Working browsing through slider. * Small styling. * Hide time travel button behind the feature flag. * Fixed actions. * Elements positioning corner cases. * Removed nodes delta buffering code. * Fixed the flow. * Fixed almost all API call cases. * Final touches * Fixed the tests. * Fix resource view updates when time travelling. * Added some comments. * Addressed some of @foot's comments.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import MockDate from 'mockdate';
|
||||
import moment from 'moment';
|
||||
import { Map as makeMap, OrderedMap as makeOrderedMap } from 'immutable';
|
||||
|
||||
import { buildUrlQuery, basePath, getApiPath, getWebsocketUrl } from '../web-api-utils';
|
||||
@@ -26,20 +26,11 @@ describe('WebApiUtils', () => {
|
||||
describe('buildUrlQuery', () => {
|
||||
let state = makeMap();
|
||||
|
||||
beforeEach(() => {
|
||||
MockDate.set(1434319925275);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
MockDate.reset();
|
||||
});
|
||||
|
||||
it('should handle empty options', () => {
|
||||
expect(buildUrlQuery(makeOrderedMap([]), state)).toBe('');
|
||||
});
|
||||
|
||||
it('should combine multiple options', () => {
|
||||
state = state.set('timeTravelMillisecondsInPast', 0);
|
||||
expect(buildUrlQuery(makeOrderedMap([
|
||||
['foo', 2],
|
||||
['bar', 4]
|
||||
@@ -47,7 +38,7 @@ describe('WebApiUtils', () => {
|
||||
});
|
||||
|
||||
it('should combine multiple options with a timestamp', () => {
|
||||
state = state.set('timeTravelMillisecondsInPast', 60 * 60 * 1000); // 1h in the past
|
||||
state = state.set('pausedAt', moment('2015-06-14T21:12:05.275Z'));
|
||||
expect(buildUrlQuery(makeOrderedMap([
|
||||
['foo', 2],
|
||||
['bar', 4]
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import debug from 'debug';
|
||||
import { union, size, map, find, reject, each } from 'lodash';
|
||||
|
||||
const log = debug('scope:nodes-delta-utils');
|
||||
|
||||
|
||||
// TODO: It would be nice to have a unit test for this function.
|
||||
export function consolidateNodesDeltas(first, second) {
|
||||
let toAdd = union(first.add, second.add);
|
||||
let toUpdate = union(first.update, second.update);
|
||||
let toRemove = union(first.remove, second.remove);
|
||||
log('Consolidating delta buffer',
|
||||
'add', size(toAdd),
|
||||
'update', size(toUpdate),
|
||||
'remove', size(toRemove));
|
||||
|
||||
// check if an added node in first was updated in second -> add second update
|
||||
toAdd = map(toAdd, (node) => {
|
||||
const updateNode = find(second.update, {id: node.id});
|
||||
if (updateNode) {
|
||||
toUpdate = reject(toUpdate, {id: node.id});
|
||||
return updateNode;
|
||||
}
|
||||
return node;
|
||||
});
|
||||
|
||||
// check if an updated node in first was updated in second -> updated second update
|
||||
// no action needed, successive updates are fine
|
||||
|
||||
// check if an added node in first was removed in second -> dont add, dont remove
|
||||
each(first.add, (node) => {
|
||||
const removedNode = find(second.remove, {id: node.id});
|
||||
if (removedNode) {
|
||||
toAdd = reject(toAdd, {id: node.id});
|
||||
toRemove = reject(toRemove, {id: node.id});
|
||||
}
|
||||
});
|
||||
|
||||
// check if an updated node in first was removed in second -> remove
|
||||
each(first.update, (node) => {
|
||||
const removedNode = find(second.remove, {id: node.id});
|
||||
if (removedNode) {
|
||||
toUpdate = reject(toUpdate, {id: node.id});
|
||||
}
|
||||
});
|
||||
|
||||
// check if an removed node in first was added in second -> update
|
||||
// remove -> add is fine for the store
|
||||
|
||||
log('Consolidated delta buffer',
|
||||
'add', size(toAdd),
|
||||
'update', size(toUpdate),
|
||||
'remove', size(toRemove));
|
||||
|
||||
return {
|
||||
add: toAdd.length > 0 ? toAdd : null,
|
||||
update: toUpdate.length > 0 ? toUpdate : null,
|
||||
remove: toRemove.length > 0 ? toRemove : null
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { endsWith } from 'lodash';
|
||||
import { Set as makeSet, List as makeList } from 'immutable';
|
||||
|
||||
import { isNowSelector } from '../selectors/time-travel';
|
||||
import { isPausedSelector } from '../selectors/time-travel';
|
||||
import { isResourceViewModeSelector } from '../selectors/topology';
|
||||
import { pinnedMetricSelector } from '../selectors/node-metric';
|
||||
import { shownNodesSelector, shownResourceTopologyIdsSelector } from '../selectors/node-filters';
|
||||
@@ -139,7 +139,7 @@ export function isTopologyNodeCountZero(state) {
|
||||
const nodeCount = state.getIn(['currentTopology', 'stats', 'node_count'], 0);
|
||||
// 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);
|
||||
return nodeCount === 0 && !isPausedSelector(state) && !isResourceViewModeSelector(state);
|
||||
}
|
||||
|
||||
export function isNodesDisplayEmpty(state) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import debug from 'debug';
|
||||
import moment from 'moment';
|
||||
import reqwest from 'reqwest';
|
||||
import { defaults } from 'lodash';
|
||||
import { Map as makeMap, List } from 'immutable';
|
||||
@@ -8,12 +7,12 @@ import { blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveEr
|
||||
receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError,
|
||||
receiveControlNodeRemoved, receiveControlPipe, receiveControlPipeStatus,
|
||||
receiveControlSuccess, receiveTopologies, receiveNotFound,
|
||||
receiveNodesForTopology } from '../actions/app-actions';
|
||||
receiveNodesForTopology, receiveNodes } from '../actions/app-actions';
|
||||
|
||||
import { getCurrentTopologyUrl } from '../utils/topology-utils';
|
||||
import { layersTopologyIdsSelector } from '../selectors/resource-view/layout';
|
||||
import { activeTopologyOptionsSelector } from '../selectors/topology';
|
||||
import { isNowSelector } from '../selectors/time-travel';
|
||||
import { isPausedSelector } from '../selectors/time-travel';
|
||||
|
||||
import { API_REFRESH_INTERVAL, TOPOLOGY_REFRESH_INTERVAL } from '../constants/timer';
|
||||
|
||||
@@ -50,10 +49,9 @@ let continuePolling = true;
|
||||
|
||||
export function getSerializedTimeTravelTimestamp(state) {
|
||||
// The timestamp parameter will be used only if it's in the past.
|
||||
if (isNowSelector(state)) return null;
|
||||
if (!isPausedSelector(state)) return null;
|
||||
|
||||
const millisecondsInPast = state.get('timeTravelMillisecondsInPast');
|
||||
return moment().utc().subtract(millisecondsInPast).toISOString();
|
||||
return state.get('pausedAt').toISOString();
|
||||
}
|
||||
|
||||
export function buildUrlQuery(params = makeMap(), state) {
|
||||
@@ -114,7 +112,7 @@ function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), state) {
|
||||
return `${getWebsocketUrl()}${topologyUrl}/ws?${optionsQuery}`;
|
||||
}
|
||||
|
||||
function createWebsocket(websocketUrl, dispatch) {
|
||||
function createWebsocket(websocketUrl, getState, dispatch) {
|
||||
if (socket) {
|
||||
socket.onclose = null;
|
||||
socket.onerror = null;
|
||||
@@ -140,9 +138,9 @@ function createWebsocket(websocketUrl, dispatch) {
|
||||
socket = null;
|
||||
dispatch(closeWebsocket());
|
||||
|
||||
if (continuePolling) {
|
||||
if (continuePolling && !isPausedSelector(getState())) {
|
||||
reconnectTimer = setTimeout(() => {
|
||||
createWebsocket(websocketUrl, dispatch);
|
||||
createWebsocket(websocketUrl, getState, dispatch);
|
||||
}, reconnectTimerInterval);
|
||||
}
|
||||
};
|
||||
@@ -199,6 +197,24 @@ function getNodesForTopologies(state, dispatch, topologyIds, topologyOptions = m
|
||||
Promise.resolve());
|
||||
}
|
||||
|
||||
function getNodesOnce(getState, dispatch) {
|
||||
const state = getState();
|
||||
const topologyUrl = getCurrentTopologyUrl(state);
|
||||
const topologyOptions = activeTopologyOptionsSelector(state);
|
||||
const optionsQuery = buildUrlQuery(topologyOptions, state);
|
||||
const url = `${getApiPath()}${topologyUrl}?${optionsQuery}`;
|
||||
doRequest({
|
||||
url,
|
||||
success: (res) => {
|
||||
dispatch(receiveNodes(res.nodes));
|
||||
},
|
||||
error: (req) => {
|
||||
log(`Error in nodes request: ${req.responseText}`);
|
||||
dispatch(receiveError(url));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets nodes for all topologies (for search).
|
||||
*/
|
||||
@@ -217,21 +233,20 @@ export function getResourceViewNodesSnapshot(state, dispatch) {
|
||||
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;
|
||||
// NOTE: getState is called every time to make sure the up-to-date state is used.
|
||||
export function getTopologies(getState, dispatch, initialPoll = false) {
|
||||
// Used to resume polling when navigating between pages in Weave Cloud.
|
||||
continuePolling = initialPoll === true ? true : continuePolling;
|
||||
clearTimeout(topologyTimer);
|
||||
const optionsQuery = buildUrlQuery(activeTopologyOptionsSelector(state), state);
|
||||
const optionsQuery = buildUrlQuery(activeTopologyOptionsSelector(getState()), getState());
|
||||
const url = `${getApiPath()}/api/topology?${optionsQuery}`;
|
||||
doRequest({
|
||||
url,
|
||||
success: (res) => {
|
||||
if (continuePolling) {
|
||||
if (continuePolling && !isPausedSelector(getState())) {
|
||||
dispatch(receiveTopologies(res));
|
||||
topologyTimer = setTimeout(() => {
|
||||
getTopologies(state, dispatch);
|
||||
getTopologies(getState, dispatch);
|
||||
}, TOPOLOGY_REFRESH_INTERVAL);
|
||||
}
|
||||
},
|
||||
@@ -239,30 +254,31 @@ export function getTopologies(state, dispatch, initialPoll = false) {
|
||||
log(`Error in topology request: ${req.responseText}`);
|
||||
dispatch(receiveError(url));
|
||||
// Only retry in stand-alone mode
|
||||
if (continuePolling) {
|
||||
if (continuePolling && !isPausedSelector(getState())) {
|
||||
topologyTimer = setTimeout(() => {
|
||||
getTopologies(state, dispatch);
|
||||
getTopologies(getState, dispatch);
|
||||
}, TOPOLOGY_REFRESH_INTERVAL);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function updateWebsocketChannel(state, dispatch) {
|
||||
const topologyUrl = getCurrentTopologyUrl(state);
|
||||
const topologyOptions = activeTopologyOptionsSelector(state);
|
||||
const websocketUrl = buildWebsocketUrl(topologyUrl, topologyOptions, state);
|
||||
function updateWebsocketChannel(getState, dispatch, forceRequest) {
|
||||
const topologyUrl = getCurrentTopologyUrl(getState());
|
||||
const topologyOptions = activeTopologyOptionsSelector(getState());
|
||||
const websocketUrl = buildWebsocketUrl(topologyUrl, topologyOptions, getState());
|
||||
// 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
|
||||
// and no socket exists, or if we get a new url.
|
||||
if (topologyUrl && (!socket || isNewUrl)) {
|
||||
createWebsocket(websocketUrl, dispatch);
|
||||
if (topologyUrl && (!socket || isNewUrl || forceRequest)) {
|
||||
createWebsocket(websocketUrl, getState, dispatch);
|
||||
currentUrl = websocketUrl;
|
||||
}
|
||||
}
|
||||
|
||||
export function getNodeDetails(state, dispatch) {
|
||||
export function getNodeDetails(getState, dispatch) {
|
||||
const state = getState();
|
||||
const nodeMap = state.get('nodeDetails');
|
||||
const topologyUrlsById = state.get('topologyUrlsById');
|
||||
const currentTopologyId = state.get('currentTopologyId');
|
||||
@@ -302,6 +318,15 @@ export function getNodeDetails(state, dispatch) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getNodes(getState, dispatch, forceRequest = false) {
|
||||
if (isPausedSelector(getState())) {
|
||||
getNodesOnce(getState, dispatch);
|
||||
} else {
|
||||
updateWebsocketChannel(getState, dispatch, forceRequest);
|
||||
}
|
||||
getNodeDetails(getState, dispatch);
|
||||
}
|
||||
|
||||
export function getApiDetails(dispatch) {
|
||||
clearTimeout(apiDetailsTimer);
|
||||
const url = `${getApiPath()}/api`;
|
||||
|
||||
Reference in New Issue
Block a user