Resolved dependency cycle between actions and web utils

This commit is contained in:
Filip Barl
2019-08-02 20:02:30 +02:00
parent c2fa42f514
commit 478e325755
19 changed files with 644 additions and 607 deletions

View File

@@ -1,45 +1,19 @@
import debug from 'debug';
import { fromJS } from 'immutable';
import ActionTypes from '../constants/action-types';
import { saveGraph } from '../utils/file-utils';
import { clearStoredViewState, updateRoute } from '../utils/router-utils';
import {
doRequest,
getApiPath,
getAllNodes,
getResourceViewNodesSnapshot,
getNodeDetails,
getTopologies,
deletePipe,
stopPolling,
teardownWebsockets,
getNodes,
} from '../utils/web-api-utils';
import { isPausedSelector } from '../selectors/time-travel';
import {
availableMetricTypesSelector,
nextPinnedMetricTypeSelector,
previousPinnedMetricTypeSelector,
pinnedMetricSelector,
} from '../selectors/node-metric';
import {
isResourceViewModeSelector,
resourceViewAvailableSelector,
} from '../selectors/topology';
import { isResourceViewModeSelector } from '../selectors/topology';
import {
GRAPH_VIEW_MODE,
TABLE_VIEW_MODE,
RESOURCE_VIEW_MODE,
} from '../constants/naming';
let controlErrorTimer = 0;
const log = debug('scope:app-actions');
export function showHelp() {
return { type: ActionTypes.SHOW_HELP };
}
@@ -184,41 +158,10 @@ export function updateSearch(searchQuery = '', pinnedSearches = []) {
};
}
export function focusSearch() {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.FOCUS_SEARCH });
// update nodes cache to allow search across all topologies,
// wait a second until animation is over
// NOTE: This will cause matching recalculation (and rerendering)
// of all the nodes in the topology, instead applying it only on
// the nodes delta. The solution would be to implement deeper
// search selectors with per-node caching instead of per-topology.
setTimeout(() => {
getAllNodes(getState(), dispatch);
}, 1200);
};
}
export function blurSearch() {
return { type: ActionTypes.BLUR_SEARCH };
}
export function changeTopologyOption(option, value, topologyId, addOrRemove) {
return (dispatch, getState) => {
dispatch({
addOrRemove,
option,
topologyId,
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
value
});
updateRoute(getState);
// update all request workers with new options
getTopologies(getState, dispatch);
getNodes(getState, dispatch);
};
}
export function clickBackground() {
return (dispatch, getState) => {
dispatch({
@@ -228,18 +171,6 @@ export function clickBackground() {
};
}
export function clickCloseDetails(nodeId) {
return (dispatch, getState) => {
dispatch({
nodeId,
type: ActionTypes.CLICK_CLOSE_DETAILS
});
// Pull the most recent details for the next details panel that comes into focus.
getNodeDetails(getState, dispatch);
updateRoute(getState);
};
}
export function closeTerminal(pipeId) {
return (dispatch, getState) => {
dispatch({
@@ -300,102 +231,6 @@ export function setTableView() {
};
}
export function setResourceView() {
return (dispatch, getState) => {
if (resourceViewAvailableSelector(getState())) {
dispatch({
type: ActionTypes.SET_VIEW_MODE,
viewMode: RESOURCE_VIEW_MODE,
});
// Pin the first metric if none of the visible ones is pinned.
const state = getState();
if (!pinnedMetricSelector(state)) {
const firstAvailableMetricType = availableMetricTypesSelector(state).first();
dispatch(pinMetric(firstAvailableMetricType));
}
getResourceViewNodesSnapshot(getState(), dispatch);
updateRoute(getState);
}
};
}
export function clickNode(nodeId, label, origin, topologyId = null) {
return (dispatch, getState) => {
dispatch({
label,
nodeId,
origin,
topologyId,
type: ActionTypes.CLICK_NODE,
});
updateRoute(getState);
getNodeDetails(getState, dispatch);
};
}
export function pauseTimeAtNow() {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.PAUSE_TIME_AT_NOW
});
updateRoute(getState);
if (!getState().get('nodesLoaded')) {
getNodes(getState, dispatch);
if (isResourceViewModeSelector(getState())) {
getResourceViewNodesSnapshot(getState(), dispatch);
}
}
};
}
export function clickRelative(nodeId, topologyId, label, origin) {
return (dispatch, getState) => {
dispatch({
label,
nodeId,
origin,
topologyId,
type: ActionTypes.CLICK_RELATIVE
});
updateRoute(getState);
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(state, dispatch);
}
updateRoute(getState);
// NOTE: This is currently not needed for our static resource
// view, but we'll need it here later and it's simpler to just
// keep it than to redo the nodes delta updating logic.
getNodes(getState, dispatch);
}
export function clickShowTopologyForNode(topologyId, nodeId) {
return (dispatch, getState) => {
dispatch({
nodeId,
topologyId,
type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE
});
updateTopology(dispatch, getState);
};
}
export function clickTopology(topologyId) {
return (dispatch, getState) => {
dispatch({
topologyId,
type: ActionTypes.CLICK_TOPOLOGY
});
updateTopology(dispatch, getState);
};
}
export function cacheZoomState(zoomState) {
return {
type: ActionTypes.CACHE_ZOOM_STATE,
@@ -520,23 +355,6 @@ export function receiveNodesDelta(delta) {
};
}
export function resumeTime() {
return (dispatch, getState) => {
if (isPausedSelector(getState())) {
dispatch({
type: ActionTypes.RESUME_TIME
});
updateRoute(getState);
// After unpausing, all of the following calls will re-activate polling.
getTopologies(getState, dispatch);
getNodes(getState, dispatch, true);
if (isResourceViewModeSelector(getState())) {
getResourceViewNodesSnapshot(getState(), dispatch);
}
}
};
}
export function receiveNodes(nodes) {
return {
nodes,
@@ -544,26 +362,6 @@ export function receiveNodes(nodes) {
};
}
export function jumpToTime(timestamp) {
return (dispatch, getState) => {
dispatch({
timestamp,
type: ActionTypes.JUMP_TO_TIME,
});
updateRoute(getState);
getTopologies(getState, dispatch);
if (!getState().get('nodesLoaded')) {
getNodes(getState, dispatch);
if (isResourceViewModeSelector(getState())) {
getResourceViewNodesSnapshot(getState(), dispatch);
}
} else {
// Get most recent details before freezing the state.
getNodeDetails(getState, dispatch);
}
};
}
export function receiveNodesForTopology(nodes, topologyId) {
return {
nodes,
@@ -572,53 +370,6 @@ export function receiveNodesForTopology(nodes, topologyId) {
};
}
export function receiveTopologies(topologies) {
return (dispatch, getState) => {
const firstLoad = !getState().get('topologiesLoaded');
dispatch({
topologies,
type: ActionTypes.RECEIVE_TOPOLOGIES
});
getNodes(getState, dispatch);
// Populate search matches on first load
const state = getState();
// Fetch all the relevant nodes once on first load
if (firstLoad && isResourceViewModeSelector(state)) {
getResourceViewNodesSnapshot(state, dispatch);
}
};
}
export function receiveApiDetails(apiDetails) {
return (dispatch, getState) => {
const isFirstTime = !getState().get('version');
const pausedAt = getState().get('pausedAt');
dispatch({
capabilities: fromJS(apiDetails.capabilities || {}),
hostname: apiDetails.hostname,
newVersion: apiDetails.newVersion,
plugins: apiDetails.plugins,
type: ActionTypes.RECEIVE_API_DETAILS,
version: apiDetails.version,
});
// On initial load either start time travelling at the pausedAt timestamp
// (if it was given as URL param) if time travelling is enabled, otherwise
// simply pause at the present time which is arguably the next best thing
// we could do.
// NOTE: We can't make this decision before API details are received because
// we have no prior info on whether time travel would be available.
if (isFirstTime && pausedAt) {
if (apiDetails.capabilities && apiDetails.capabilities.historic_reports) {
dispatch(jumpToTime(pausedAt));
} else {
dispatch(pauseTimeAtNow());
}
}
};
}
export function receiveControlNodeRemoved(nodeId) {
return (dispatch, getState) => {
dispatch({
@@ -639,34 +390,6 @@ export function receiveControlPipeFromParams(pipeId, rawTty, resizeTtyControl) {
};
}
export function receiveControlPipe(pipeId, nodeId, rawTty, resizeTtyControl, control) {
return (dispatch, getState) => {
const state = getState();
if (state.get('nodeDetails').last()
&& nodeId !== state.get('nodeDetails').last().id) {
log('Node was deselected before we could set up control!');
deletePipe(pipeId, dispatch);
return;
}
const controlPipe = state.get('controlPipes').last();
if (controlPipe && controlPipe.get('id') !== pipeId) {
deletePipe(controlPipe.get('id'), dispatch);
}
dispatch({
control,
nodeId,
pipeId,
rawTty,
resizeTtyControl,
type: ActionTypes.RECEIVE_CONTROL_PIPE
});
updateRoute(getState);
};
}
export function receiveControlPipeStatus(pipeId, status) {
return {
pipeId,
@@ -675,53 +398,6 @@ export function receiveControlPipeStatus(pipeId, status) {
};
}
function doControlRequest(nodeId, control, dispatch) {
clearTimeout(controlErrorTimer);
const url = `${getApiPath()}/api/control/${encodeURIComponent(control.probeId)}/`
+ `${encodeURIComponent(control.nodeId)}/${control.id}`;
doRequest({
error: (err) => {
dispatch(receiveControlError(nodeId, err.response));
controlErrorTimer = setTimeout(() => {
dispatch(clearControlError(nodeId));
}, 10000);
},
method: 'POST',
success: (res) => {
dispatch(receiveControlSuccess(nodeId));
if (res) {
if (res.pipe) {
dispatch(blurSearch());
const resizeTtyControl = res.resize_tty_control
&& { id: res.resize_tty_control, nodeId: control.nodeId, probeId: control.probeId };
dispatch(receiveControlPipe(
res.pipe,
nodeId,
res.raw_tty,
resizeTtyControl,
control
));
}
if (res.removedNode) {
dispatch(receiveControlNodeRemoved(nodeId));
}
}
},
url
});
}
export function doControl(nodeId, control) {
return (dispatch) => {
dispatch({
control,
nodeId,
type: ActionTypes.DO_CONTROL
});
doControlRequest(nodeId, control, dispatch);
};
}
export function receiveError(errorUrl) {
return {
errorUrl,
@@ -747,41 +423,6 @@ export function setContrastMode(enabled) {
};
}
export function getTopologiesWithInitialPoll() {
return (dispatch, getState) => {
getTopologies(getState, dispatch, true);
};
}
export function route(urlState) {
return (dispatch, getState) => {
dispatch({
state: urlState,
type: ActionTypes.ROUTE_TOPOLOGY
});
// Handle Time Travel state update through separate actions as it's more complex.
// This is mostly to handle switching contexts Explore <-> Monitor in WC while
// the timestamp keeps changing - e.g. if we were Time Travelling in Scope and
// then went live in Monitor, switching back to Explore should properly close
// the Time Travel etc, not just update the pausedAt state directly.
if (!urlState.pausedAt) {
dispatch(resumeTime());
} else {
dispatch(jumpToTime(urlState.pausedAt));
}
// update all request workers with new options
getTopologies(getState, dispatch);
getNodes(getState, 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.
const state = getState();
if (isResourceViewModeSelector(state)) {
getResourceViewNodesSnapshot(state, dispatch);
}
};
}
export function resetLocalViewState() {
return (dispatch) => {
dispatch({ type: ActionTypes.RESET_LOCAL_VIEW_STATE });
@@ -807,16 +448,6 @@ export function changeInstance() {
};
}
export function shutdown() {
return (dispatch) => {
stopPolling();
teardownWebsockets();
dispatch({
type: ActionTypes.SHUTDOWN
});
};
}
export function setMonitorState(monitor) {
return {
monitor,

View File

@@ -0,0 +1,608 @@
/*
This file consists of functions that both dispatch actions to Redux and also make API requests.
TODO: Refactor all the methods below so that the split between actions and
requests is more clear, and make user components make explicit calls to requests
and dispatch actions when handling request promises.
*/
import debug from 'debug';
import { fromJS } from 'immutable';
import ActionTypes from '../constants/action-types';
import { RESOURCE_VIEW_MODE } from '../constants/naming';
import {
API_REFRESH_INTERVAL,
TOPOLOGY_REFRESH_INTERVAL,
} from '../constants/timer';
import { updateRoute } from '../utils/router-utils';
import { getCurrentTopologyUrl } from '../utils/topology-utils';
import {
doRequest,
getApiPath,
getAllNodes,
getNodesOnce,
deletePipe,
getNodeDetails,
getResourceViewNodesSnapshot,
topologiesUrl,
buildWebsocketUrl,
} from '../utils/web-api-utils';
import {
availableMetricTypesSelector,
pinnedMetricSelector,
} from '../selectors/node-metric';
import {
isResourceViewModeSelector,
resourceViewAvailableSelector,
activeTopologyOptionsSelector,
} from '../selectors/topology';
import { isPausedSelector } from '../selectors/time-travel';
import {
receiveControlNodeRemoved,
receiveControlPipeStatus,
receiveControlSuccess,
receiveControlError,
receiveError,
pinMetric,
openWebsocket,
closeWebsocket,
receiveNodesDelta,
clearControlError,
blurSearch,
} from './app-actions';
const log = debug('scope:app-actions');
const reconnectTimerInterval = 5000;
const FIRST_RENDER_TOO_LONG_THRESHOLD = 100; // ms
let socket;
let topologyTimer = 0;
let controlErrorTimer = 0;
let reconnectTimer = 0;
let apiDetailsTimer = 0;
let continuePolling = true;
let firstMessageOnWebsocketAt = null;
let createWebsocketAt = null;
let currentUrl = null;
function createWebsocket(websocketUrl, getState, dispatch) {
if (socket) {
socket.onclose = null;
socket.onerror = null;
socket.close();
// onclose() is not called, but that's fine since we're opening a new one
// right away
}
// profiling
createWebsocketAt = new Date();
firstMessageOnWebsocketAt = null;
socket = new WebSocket(websocketUrl);
socket.onopen = () => {
log(`Opening websocket to ${websocketUrl}`);
dispatch(openWebsocket());
};
socket.onclose = () => {
clearTimeout(reconnectTimer);
log(`Closing websocket to ${websocketUrl}`, socket.readyState);
socket = null;
dispatch(closeWebsocket());
if (continuePolling && !isPausedSelector(getState())) {
reconnectTimer = setTimeout(() => {
createWebsocket(websocketUrl, getState, dispatch);
}, reconnectTimerInterval);
}
};
socket.onerror = () => {
log(`Error in websocket to ${websocketUrl}`);
dispatch(receiveError(websocketUrl));
};
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
dispatch(receiveNodesDelta(msg));
// profiling (receiveNodesDelta triggers synchronous render)
if (!firstMessageOnWebsocketAt) {
firstMessageOnWebsocketAt = new Date();
const timeToFirstMessage = firstMessageOnWebsocketAt - createWebsocketAt;
if (timeToFirstMessage > FIRST_RENDER_TOO_LONG_THRESHOLD) {
log(
'Time (ms) to first nodes render after websocket was created',
firstMessageOnWebsocketAt - createWebsocketAt
);
}
}
};
}
function teardownWebsockets() {
clearTimeout(reconnectTimer);
if (socket) {
socket.onerror = null;
socket.onclose = null;
socket.onmessage = null;
socket.onopen = null;
socket.close();
socket = null;
currentUrl = null;
}
}
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 || forceRequest)) {
createWebsocket(websocketUrl, getState, dispatch);
currentUrl = websocketUrl;
}
}
function getNodes(getState, dispatch, forceRequest = false) {
if (isPausedSelector(getState())) {
getNodesOnce(getState, dispatch);
} else {
updateWebsocketChannel(getState, dispatch, forceRequest);
}
getNodeDetails(getState, dispatch);
}
export function pauseTimeAtNow() {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.PAUSE_TIME_AT_NOW
});
updateRoute(getState);
if (!getState().get('nodesLoaded')) {
getNodes(getState, dispatch);
if (isResourceViewModeSelector(getState())) {
getResourceViewNodesSnapshot(getState(), dispatch);
}
}
};
}
function receiveTopologies(topologies) {
return (dispatch, getState) => {
const firstLoad = !getState().get('topologiesLoaded');
dispatch({
topologies,
type: ActionTypes.RECEIVE_TOPOLOGIES
});
getNodes(getState, dispatch);
// Populate search matches on first load
const state = getState();
// Fetch all the relevant nodes once on first load
if (firstLoad && isResourceViewModeSelector(state)) {
getResourceViewNodesSnapshot(state, dispatch);
}
};
}
function getTopologiesOnce(getState, dispatch) {
const url = topologiesUrl(getState());
doRequest({
error: (req) => {
log(`Error in topology request: ${req.responseText}`);
dispatch(receiveError(url));
},
success: (res) => {
dispatch(receiveTopologies(res));
},
url
});
}
function pollTopologies(getState, dispatch, initialPoll = false) {
// Used to resume polling when navigating between pages in Weave Cloud.
continuePolling = initialPoll === true ? true : continuePolling;
clearTimeout(topologyTimer);
// NOTE: getState is called every time to make sure the up-to-date state is used.
const url = topologiesUrl(getState());
doRequest({
error: (req) => {
log(`Error in topology request: ${req.responseText}`);
dispatch(receiveError(url));
// Only retry in stand-alone mode
if (continuePolling && !isPausedSelector(getState())) {
topologyTimer = setTimeout(() => {
pollTopologies(getState, dispatch);
}, TOPOLOGY_REFRESH_INTERVAL);
}
},
success: (res) => {
if (continuePolling && !isPausedSelector(getState())) {
dispatch(receiveTopologies(res));
topologyTimer = setTimeout(() => {
pollTopologies(getState, dispatch);
}, TOPOLOGY_REFRESH_INTERVAL);
}
},
url
});
}
function getTopologies(getState, dispatch, forceRequest) {
if (isPausedSelector(getState())) {
getTopologiesOnce(getState, dispatch);
} else {
pollTopologies(getState, dispatch, forceRequest);
}
}
export function jumpToTime(timestamp) {
return (dispatch, getState) => {
dispatch({
timestamp,
type: ActionTypes.JUMP_TO_TIME,
});
updateRoute(getState);
getTopologies(getState, dispatch);
if (!getState().get('nodesLoaded')) {
getNodes(getState, dispatch);
if (isResourceViewModeSelector(getState())) {
getResourceViewNodesSnapshot(getState(), dispatch);
}
} else {
// Get most recent details before freezing the state.
getNodeDetails(getState, dispatch);
}
};
}
export function receiveApiDetails(apiDetails) {
return (dispatch, getState) => {
const isFirstTime = !getState().get('version');
const pausedAt = getState().get('pausedAt');
dispatch({
capabilities: fromJS(apiDetails.capabilities || {}),
hostname: apiDetails.hostname,
newVersion: apiDetails.newVersion,
plugins: apiDetails.plugins,
type: ActionTypes.RECEIVE_API_DETAILS,
version: apiDetails.version,
});
// On initial load either start time travelling at the pausedAt timestamp
// (if it was given as URL param) if time travelling is enabled, otherwise
// simply pause at the present time which is arguably the next best thing
// we could do.
// NOTE: We can't make this decision before API details are received because
// we have no prior info on whether time travel would be available.
if (isFirstTime && pausedAt) {
if (apiDetails.capabilities && apiDetails.capabilities.historic_reports) {
dispatch(jumpToTime(pausedAt));
} else {
dispatch(pauseTimeAtNow());
}
}
};
}
export function getApiDetails(dispatch) {
clearTimeout(apiDetailsTimer);
const url = `${getApiPath()}/api`;
doRequest({
error: (req) => {
log(`Error in api details request: ${req.responseText}`);
receiveError(url);
if (continuePolling) {
apiDetailsTimer = setTimeout(() => {
getApiDetails(dispatch);
}, API_REFRESH_INTERVAL / 2);
}
},
success: (res) => {
dispatch(receiveApiDetails(res));
if (continuePolling) {
apiDetailsTimer = setTimeout(() => {
getApiDetails(dispatch);
}, API_REFRESH_INTERVAL);
}
},
url
});
}
function stopPolling() {
clearTimeout(apiDetailsTimer);
clearTimeout(topologyTimer);
continuePolling = false;
}
export function focusSearch() {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.FOCUS_SEARCH });
// update nodes cache to allow search across all topologies,
// wait a second until animation is over
// NOTE: This will cause matching recalculation (and rerendering)
// of all the nodes in the topology, instead applying it only on
// the nodes delta. The solution would be to implement deeper
// search selectors with per-node caching instead of per-topology.
setTimeout(() => {
getAllNodes(getState(), dispatch);
}, 1200);
};
}
export function getPipeStatus(pipeId, dispatch) {
const url = `${getApiPath()}/api/pipe/${encodeURIComponent(pipeId)}/check`;
doRequest({
complete: (res) => {
const status = {
204: 'PIPE_ALIVE',
404: 'PIPE_DELETED'
}[res.status];
if (!status) {
log('Unexpected pipe status:', res.status);
return;
}
dispatch(receiveControlPipeStatus(pipeId, status));
},
method: 'GET',
url
});
}
export function receiveControlPipe(pipeId, nodeId, rawTty, resizeTtyControl, control) {
return (dispatch, getState) => {
const state = getState();
if (state.get('nodeDetails').last()
&& nodeId !== state.get('nodeDetails').last().id) {
log('Node was deselected before we could set up control!');
deletePipe(pipeId, dispatch);
return;
}
const controlPipe = state.get('controlPipes').last();
if (controlPipe && controlPipe.get('id') !== pipeId) {
deletePipe(controlPipe.get('id'), dispatch);
}
dispatch({
control,
nodeId,
pipeId,
rawTty,
resizeTtyControl,
type: ActionTypes.RECEIVE_CONTROL_PIPE
});
updateRoute(getState);
};
}
function doControlRequest(nodeId, control, dispatch) {
clearTimeout(controlErrorTimer);
const url = `${getApiPath()}/api/control/${encodeURIComponent(control.probeId)}/`
+ `${encodeURIComponent(control.nodeId)}/${control.id}`;
doRequest({
error: (err) => {
dispatch(receiveControlError(nodeId, err.response));
controlErrorTimer = setTimeout(() => {
dispatch(clearControlError(nodeId));
}, 10000);
},
method: 'POST',
success: (res) => {
dispatch(receiveControlSuccess(nodeId));
if (res) {
if (res.pipe) {
dispatch(blurSearch());
const resizeTtyControl = res.resize_tty_control
&& { id: res.resize_tty_control, nodeId: control.nodeId, probeId: control.probeId };
dispatch(receiveControlPipe(
res.pipe,
nodeId,
res.raw_tty,
resizeTtyControl,
control
));
}
if (res.removedNode) {
dispatch(receiveControlNodeRemoved(nodeId));
}
}
},
url
});
}
export function doControl(nodeId, control) {
return (dispatch) => {
dispatch({
control,
nodeId,
type: ActionTypes.DO_CONTROL
});
doControlRequest(nodeId, control, dispatch);
};
}
export function shutdown() {
return (dispatch) => {
stopPolling();
teardownWebsockets();
dispatch({
type: ActionTypes.SHUTDOWN
});
};
}
export function setResourceView() {
return (dispatch, getState) => {
if (resourceViewAvailableSelector(getState())) {
dispatch({
type: ActionTypes.SET_VIEW_MODE,
viewMode: RESOURCE_VIEW_MODE,
});
// Pin the first metric if none of the visible ones is pinned.
const state = getState();
if (!pinnedMetricSelector(state)) {
const firstAvailableMetricType = availableMetricTypesSelector(state).first();
dispatch(pinMetric(firstAvailableMetricType));
}
getResourceViewNodesSnapshot(getState(), dispatch);
updateRoute(getState);
}
};
}
export function changeTopologyOption(option, value, topologyId, addOrRemove) {
return (dispatch, getState) => {
dispatch({
addOrRemove,
option,
topologyId,
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
value
});
updateRoute(getState);
// update all request workers with new options
getTopologies(getState, dispatch);
getNodes(getState, dispatch);
};
}
export function getTopologiesWithInitialPoll() {
return (dispatch, getState) => {
getTopologies(getState, dispatch, true);
};
}
export function resumeTime() {
return (dispatch, getState) => {
if (isPausedSelector(getState())) {
dispatch({
type: ActionTypes.RESUME_TIME
});
updateRoute(getState);
// After unpausing, all of the following calls will re-activate polling.
getTopologies(getState, dispatch);
getNodes(getState, dispatch, true);
if (isResourceViewModeSelector(getState())) {
getResourceViewNodesSnapshot(getState(), dispatch);
}
}
};
}
export function route(urlState) {
return (dispatch, getState) => {
dispatch({
state: urlState,
type: ActionTypes.ROUTE_TOPOLOGY
});
// Handle Time Travel state update through separate actions as it's more complex.
// This is mostly to handle switching contexts Explore <-> Monitor in WC while
// the timestamp keeps changing - e.g. if we were Time Travelling in Scope and
// then went live in Monitor, switching back to Explore should properly close
// the Time Travel etc, not just update the pausedAt state directly.
if (!urlState.pausedAt) {
dispatch(resumeTime());
} else {
dispatch(jumpToTime(urlState.pausedAt));
}
// update all request workers with new options
getTopologies(getState, dispatch);
getNodes(getState, 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.
const state = getState();
if (isResourceViewModeSelector(state)) {
getResourceViewNodesSnapshot(state, dispatch);
}
};
}
export function clickCloseDetails(nodeId) {
return (dispatch, getState) => {
dispatch({
nodeId,
type: ActionTypes.CLICK_CLOSE_DETAILS
});
// Pull the most recent details for the next details panel that comes into focus.
getNodeDetails(getState, dispatch);
updateRoute(getState);
};
}
export function clickNode(nodeId, label, origin, topologyId = null) {
return (dispatch, getState) => {
dispatch({
label,
nodeId,
origin,
topologyId,
type: ActionTypes.CLICK_NODE,
});
updateRoute(getState);
getNodeDetails(getState, dispatch);
};
}
export function clickRelative(nodeId, topologyId, label, origin) {
return (dispatch, getState) => {
dispatch({
label,
nodeId,
origin,
topologyId,
type: ActionTypes.CLICK_RELATIVE
});
updateRoute(getState);
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(state, dispatch);
}
updateRoute(getState);
// NOTE: This is currently not needed for our static resource
// view, but we'll need it here later and it's simpler to just
// keep it than to redo the nodes delta updating logic.
getNodes(getState, dispatch);
}
export function clickShowTopologyForNode(topologyId, nodeId) {
return (dispatch, getState) => {
dispatch({
nodeId,
topologyId,
type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE
});
updateTopology(dispatch, getState);
};
}
export function clickTopology(topologyId) {
return (dispatch, getState) => {
dispatch({
topologyId,
type: ActionTypes.CLICK_TOPOLOGY
});
updateTopology(dispatch, getState);
};
}

View File

@@ -7,7 +7,8 @@ import {
getMetricValue,
getMetricColor,
} from '../utils/metric-utils';
import { clickNode, enterNode, leaveNode } from '../actions/app-actions';
import { clickNode } from '../actions/request-actions';
import { enterNode, leaveNode } from '../actions/app-actions';
import { trackAnalyticsEvent } from '../utils/tracking-utils';
import { getNodeColor } from '../utils/color-utils';
import MatchedResults from '../components/matched-results';

View File

@@ -6,7 +6,8 @@ import { List as makeList, Map as makeMap } from 'immutable';
import capitalize from 'lodash/capitalize';
import NodeDetailsTable from '../components/node-details/node-details-table';
import { clickNode, sortOrderChanged } from '../actions/app-actions';
import { clickNode } from '../actions/request-actions';
import { sortOrderChanged } from '../actions/app-actions';
import { shownNodesSelector } from '../selectors/node-filters';
import { trackAnalyticsEvent } from '../utils/tracking-utils';
import { findTopologyById } from '../utils/topology-utils';

View File

@@ -18,9 +18,7 @@ import Status from './status';
import Topologies from './topologies';
import TopologyOptions from './topology-options';
import Overlay from './overlay';
import { getApiDetails } from '../utils/web-api-utils';
import {
focusSearch,
pinNextMetric,
pinPreviousMetric,
hitEsc,
@@ -29,12 +27,16 @@ import {
setGraphView,
setMonitorState,
setTableView,
setResourceView,
setStoreViewState,
shutdown,
setViewportDimensions,
getTopologiesWithInitialPoll,
} from '../actions/app-actions';
import {
focusSearch,
getApiDetails,
setResourceView,
getTopologiesWithInitialPoll,
shutdown,
} from '../actions/request-actions';
import Details from './details';
import Nodes from './nodes';
import TimeControl from './time-control';

View File

@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
import { Map as makeMap } from 'immutable';
import { noop } from 'lodash';
import { clickCloseDetails, clickShowTopologyForNode } from '../actions/app-actions';
import { clickCloseDetails, clickShowTopologyForNode } from '../actions/request-actions';
import { brightenColor, getNeutralColor, getNodeColorDark } from '../utils/color-utils';
import { isGenericTable, isPropertyList } from '../utils/node-details-utils';
import { resetDocumentTitle, setDocumentTitle } from '../utils/title-utils';

View File

@@ -4,7 +4,7 @@ import { isEmpty } from 'lodash';
import classNames from 'classnames';
import { trackAnalyticsEvent } from '../../utils/tracking-utils';
import { doControl } from '../../actions/app-actions';
import { doControl } from '../../actions/request-actions';
class NodeDetailsControlButton extends React.Component {
constructor(props, context) {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { clickRelative } from '../../actions/app-actions';
import { clickRelative } from '../../actions/request-actions';
import { trackAnalyticsEvent } from '../../utils/tracking-utils';
import MatchedText from '../matched-text';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { clickRelative } from '../../actions/app-actions';
import { clickRelative } from '../../actions/request-actions';
import { trackAnalyticsEvent } from '../../utils/tracking-utils';
import { dismissRowClickProps } from '../../utils/dom-utils';

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import theme from 'weaveworks-ui-components/lib/theme';
import NodeResourcesMetricBoxInfo from './node-resources-metric-box-info';
import { clickNode } from '../../actions/app-actions';
import { clickNode } from '../../actions/request-actions';
import { trackAnalyticsEvent } from '../../utils/tracking-utils';
import { applyTransform } from '../../utils/transform-utils';
import { RESOURCE_VIEW_MODE } from '../../constants/naming';

View File

@@ -5,8 +5,11 @@ import { Search } from 'weaveworks-ui-components';
import styled from 'styled-components';
import {
blurSearch, focusSearch, updateSearch, toggleHelp
blurSearch, updateSearch, toggleHelp
} from '../actions/app-actions';
import {
focusSearch
} from '../actions/request-actions';
import { searchMatchCountByTopologySelector } from '../selectors/search';
import { isResourceViewModeSelector } from '../selectors/topology';
import { slugify } from '../utils/string-utils';

View File

@@ -8,10 +8,11 @@ import { Terminal as Term } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit';
import { closeTerminal } from '../actions/app-actions';
import { getPipeStatus } from '../actions/request-actions';
import { getNeutralColor } from '../utils/color-utils';
import { setDocumentTitle } from '../utils/title-utils';
import {
getPipeStatus, deletePipe, doResizeTty, getWebsocketUrl, basePath
deletePipe, doResizeTty, getWebsocketUrl, basePath
} from '../utils/web-api-utils';
const log = debug('scope:terminal');

View File

@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { TimestampTag } from 'weaveworks-ui-components';
import { trackAnalyticsEvent } from '../utils/tracking-utils';
import { pauseTimeAtNow, resumeTime } from '../actions/app-actions';
import { pauseTimeAtNow, resumeTime } from '../actions/request-actions';
import { isPausedSelector, timeTravelSupportedSelector } from '../selectors/time-travel';

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { TimeTravel } from 'weaveworks-ui-components';
import { jumpToTime, resumeTime, pauseTimeAtNow } from '../actions/app-actions';
import { jumpToTime, resumeTime, pauseTimeAtNow } from '../actions/request-actions';
class TimeTravelWrapper extends React.Component {
handleLiveModeChange = (showingLive) => {

View File

@@ -5,7 +5,7 @@ import classnames from 'classnames';
import { trackAnalyticsEvent } from '../utils/tracking-utils';
import { searchMatchCountByTopologySelector } from '../selectors/search';
import { isResourceViewModeSelector } from '../selectors/topology';
import { clickTopology } from '../actions/app-actions';
import { clickTopology } from '../actions/request-actions';
function basicTopologyInfo(topology, searchMatchCount) {

View File

@@ -7,7 +7,7 @@ import { trackAnalyticsEvent } from '../utils/tracking-utils';
import { getCurrentTopologyOptions } from '../utils/topology-utils';
import { activeTopologyOptionsSelector } from '../selectors/topology';
import TopologyOptionAction from './topology-option-action';
import { changeTopologyOption } from '../actions/app-actions';
import { changeTopologyOption } from '../actions/request-actions';
class TopologyOptions extends React.Component {
constructor(props, context) {

View File

@@ -3,7 +3,8 @@ import { connect } from 'react-redux';
import ViewModeButton from './view-mode-button';
import MetricSelector from './metric-selector';
import { setGraphView, setTableView, setResourceView } from '../actions/app-actions';
import { setResourceView } from '../actions/request-actions';
import { setGraphView, setTableView } from '../actions/app-actions';
import { availableMetricsSelector } from '../selectors/node-metric';
import {
isResourceViewModeSelector,

View File

@@ -2,7 +2,7 @@ import page from 'page';
import stableStringify from 'json-stable-stringify';
import { each } from 'lodash';
import { route } from './actions/app-actions';
import { route } from './actions/request-actions';
import { storageGet, storageSet } from './utils/storage-utils';
import {
decodeURL, encodeURL, isStoreViewStateEnabled, STORAGE_STATE_KEY

View File

@@ -4,24 +4,18 @@ import { defaults } from 'lodash';
import { Map as makeMap, List } from 'immutable';
import {
closeWebsocket, openWebsocket, receiveError,
receiveApiDetails, receiveNodesDelta, receiveNodeDetails,
receiveControlPipeStatus, receiveTopologies, receiveNotFound,
receiveNodesForTopology, receiveNodes,
receiveError,
receiveNodeDetails,
receiveNotFound, receiveNodesForTopology, receiveNodes,
} from '../actions/app-actions';
import { getCurrentTopologyUrl } from './topology-utils';
import { layersTopologyIdsSelector } from '../selectors/resource-view/layout';
import { activeTopologyOptionsSelector } from '../selectors/topology';
import { isPausedSelector } from '../selectors/time-travel';
import { API_REFRESH_INTERVAL, TOPOLOGY_REFRESH_INTERVAL } from '../constants/timer';
const log = debug('scope:web-api-utils');
const reconnectTimerInterval = 5000;
const updateFrequency = '5s';
const FIRST_RENDER_TOO_LONG_THRESHOLD = 100; // ms
const csrfToken = (() => {
// Check for token at window level or parent level (for iframe);
/* eslint-disable no-underscore-dangle */
@@ -37,15 +31,6 @@ const csrfToken = (() => {
return token;
})();
let socket;
let reconnectTimer = 0;
let topologyTimer = 0;
let apiDetailsTimer = 0;
let currentUrl = null;
let createWebsocketAt = null;
let firstMessageOnWebsocketAt = null;
let continuePolling = true;
export function buildUrlQuery(params = makeMap(), state = null) {
// Attach the time travel timestamp to every request to the backend.
if (state) {
@@ -103,7 +88,7 @@ export function getReportUrl(timestamp) {
return `${getApiPath()}/api/report?${buildUrlQuery(makeMap({ timestamp }))}`;
}
function topologiesUrl(state) {
export function topologiesUrl(state) {
const activeTopologyOptions = activeTopologyOptionsSelector(state);
const optionsQuery = buildUrlQuery(activeTopologyOptions, state);
return `${getApiPath()}/api/topology?${optionsQuery}`;
@@ -114,68 +99,12 @@ export function getWebsocketUrl(host = window.location.host, pathname = window.l
return `${wsProto}://${host}${getApiPath(pathname)}`;
}
function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), state) {
export function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), state) {
topologyOptions = topologyOptions.set('t', updateFrequency);
const optionsQuery = buildUrlQuery(topologyOptions, state);
return `${getWebsocketUrl()}${topologyUrl}/ws?${optionsQuery}`;
}
function createWebsocket(websocketUrl, getState, dispatch) {
if (socket) {
socket.onclose = null;
socket.onerror = null;
socket.close();
// onclose() is not called, but that's fine since we're opening a new one
// right away
}
// profiling
createWebsocketAt = new Date();
firstMessageOnWebsocketAt = null;
socket = new WebSocket(websocketUrl);
socket.onopen = () => {
log(`Opening websocket to ${websocketUrl}`);
dispatch(openWebsocket());
};
socket.onclose = () => {
clearTimeout(reconnectTimer);
log(`Closing websocket to ${websocketUrl}`, socket.readyState);
socket = null;
dispatch(closeWebsocket());
if (continuePolling && !isPausedSelector(getState())) {
reconnectTimer = setTimeout(() => {
createWebsocket(websocketUrl, getState, dispatch);
}, reconnectTimerInterval);
}
};
socket.onerror = () => {
log(`Error in websocket to ${websocketUrl}`);
dispatch(receiveError(websocketUrl));
};
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
dispatch(receiveNodesDelta(msg));
// profiling (receiveNodesDelta triggers synchronous render)
if (!firstMessageOnWebsocketAt) {
firstMessageOnWebsocketAt = new Date();
const timeToFirstMessage = firstMessageOnWebsocketAt - createWebsocketAt;
if (timeToFirstMessage > FIRST_RENDER_TOO_LONG_THRESHOLD) {
log(
'Time (ms) to first nodes render after websocket was created',
firstMessageOnWebsocketAt - createWebsocketAt
);
}
}
};
}
/**
* XHR wrapper. Applies a CSRF token (if it exists) and content-type to all requests.
* Any opts that get passed in will override the defaults.
@@ -210,7 +139,7 @@ function getNodesForTopologies(state, dispatch, topologyIds, topologyOptions = m
);
}
function getNodesOnce(getState, dispatch) {
export function getNodesOnce(getState, dispatch) {
const state = getState();
const topologyUrl = getCurrentTopologyUrl(state);
const topologyOptions = activeTopologyOptionsSelector(state);
@@ -246,63 +175,6 @@ export function getResourceViewNodesSnapshot(state, dispatch) {
getNodesForTopologies(state, dispatch, topologyIds);
}
function pollTopologies(getState, dispatch, initialPoll = false) {
// Used to resume polling when navigating between pages in Weave Cloud.
continuePolling = initialPoll === true ? true : continuePolling;
clearTimeout(topologyTimer);
// NOTE: getState is called every time to make sure the up-to-date state is used.
const url = topologiesUrl(getState());
doRequest({
error: (req) => {
log(`Error in topology request: ${req.responseText}`);
dispatch(receiveError(url));
// Only retry in stand-alone mode
if (continuePolling && !isPausedSelector(getState())) {
topologyTimer = setTimeout(() => {
pollTopologies(getState, dispatch);
}, TOPOLOGY_REFRESH_INTERVAL);
}
},
success: (res) => {
if (continuePolling && !isPausedSelector(getState())) {
dispatch(receiveTopologies(res));
topologyTimer = setTimeout(() => {
pollTopologies(getState, dispatch);
}, TOPOLOGY_REFRESH_INTERVAL);
}
},
url
});
}
function getTopologiesOnce(getState, dispatch) {
const url = topologiesUrl(getState());
doRequest({
error: (req) => {
log(`Error in topology request: ${req.responseText}`);
dispatch(receiveError(url));
},
success: (res) => {
dispatch(receiveTopologies(res));
},
url
});
}
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 || forceRequest)) {
createWebsocket(websocketUrl, getState, dispatch);
currentUrl = websocketUrl;
}
}
export function getNodeDetails(getState, dispatch) {
const state = getState();
const nodeMap = state.get('nodeDetails');
@@ -349,48 +221,6 @@ export function getNodeDetails(getState, dispatch) {
}
}
export function getTopologies(getState, dispatch, forceRequest) {
if (isPausedSelector(getState())) {
getTopologiesOnce(getState, dispatch);
} else {
pollTopologies(getState, dispatch, forceRequest);
}
}
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`;
doRequest({
error: (req) => {
log(`Error in api details request: ${req.responseText}`);
receiveError(url);
if (continuePolling) {
apiDetailsTimer = setTimeout(() => {
getApiDetails(dispatch);
}, API_REFRESH_INTERVAL / 2);
}
},
success: (res) => {
dispatch(receiveApiDetails(res));
if (continuePolling) {
apiDetailsTimer = setTimeout(() => {
getApiDetails(dispatch);
}, API_REFRESH_INTERVAL);
}
},
url
});
}
export function doResizeTty(pipeId, control, cols, rows) {
const url = `${getApiPath()}/api/control/${encodeURIComponent(control.probeId)}/`
+ `${encodeURIComponent(control.nodeId)}/${control.id}`;
@@ -420,44 +250,3 @@ export function deletePipe(pipeId, dispatch) {
url
});
}
export function getPipeStatus(pipeId, dispatch) {
const url = `${getApiPath()}/api/pipe/${encodeURIComponent(pipeId)}/check`;
doRequest({
complete: (res) => {
const status = {
204: 'PIPE_ALIVE',
404: 'PIPE_DELETED'
}[res.status];
if (!status) {
log('Unexpected pipe status:', res.status);
return;
}
dispatch(receiveControlPipeStatus(pipeId, status));
},
method: 'GET',
url
});
}
export function stopPolling() {
clearTimeout(apiDetailsTimer);
clearTimeout(topologyTimer);
continuePolling = false;
}
export function teardownWebsockets() {
clearTimeout(reconnectTimer);
if (socket) {
socket.onerror = null;
socket.onclose = null;
socket.onmessage = null;
socket.onopen = null;
socket.close();
socket = null;
currentUrl = null;
}
}