mirror of
https://github.com/weaveworks/scope.git
synced 2026-02-14 18:09:59 +00:00
Resolved dependency cycle between actions and web utils
This commit is contained in:
@@ -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,
|
||||
|
||||
608
client/app/scripts/actions/request-actions.js
Normal file
608
client/app/scripts/actions/request-actions.js
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user