mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 01:31:17 +00:00
Migrate from Flux to Redux
* better state visibility * pure state changes * state debug panel (show: crtl-h, move: ctrl-w)
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import page from 'page';
|
||||
|
||||
import { route } from '../actions/app-actions';
|
||||
import AppStore from '../stores/app-store';
|
||||
|
||||
//
|
||||
// page.js won't match the routes below if ":state" has a slash in it, so replace those before we
|
||||
@@ -27,8 +26,24 @@ function shouldReplaceState(prevState, nextState) {
|
||||
return terminalToTerminal || closingTheTerminal;
|
||||
}
|
||||
|
||||
export function updateRoute() {
|
||||
const state = AppStore.getAppState();
|
||||
export function getUrlState(state) {
|
||||
const cp = state.get('controlPipes').last();
|
||||
const nodeDetails = state.get('nodeDetails').toIndexedSeq().map(details => ({
|
||||
id: details.id, label: details.label, topologyId: details.topologyId
|
||||
}));
|
||||
|
||||
return {
|
||||
controlPipe: cp ? cp.toJS() : null,
|
||||
nodeDetails: nodeDetails.toJS(),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
pinnedMetricType: state.get('pinnedMetricType'),
|
||||
topologyId: state.get('currentTopologyId'),
|
||||
topologyOptions: state.get('topologyOptions').toJS() // all options
|
||||
};
|
||||
}
|
||||
|
||||
export function updateRoute(getState) {
|
||||
const state = getUrlState(getState());
|
||||
const stateUrl = encodeURL(JSON.stringify(state));
|
||||
const dispatch = false;
|
||||
const urlStateString = window.location.hash
|
||||
@@ -44,17 +59,19 @@ export function updateRoute() {
|
||||
}
|
||||
}
|
||||
|
||||
page('/', () => {
|
||||
updateRoute();
|
||||
});
|
||||
|
||||
page('/state/:state', (ctx) => {
|
||||
const state = JSON.parse(ctx.params.state);
|
||||
route(state);
|
||||
});
|
||||
|
||||
export function getRouter() {
|
||||
export function getRouter(dispatch, initialState) {
|
||||
// strip any trailing '/'s.
|
||||
page.base(window.location.pathname.replace(/\/$/, ''));
|
||||
|
||||
page('/', () => {
|
||||
dispatch(route(initialState));
|
||||
});
|
||||
|
||||
page('/state/:state', (ctx) => {
|
||||
const state = JSON.parse(ctx.params.state);
|
||||
dispatch(route(state));
|
||||
});
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { is as isDeepEqual, Map as makeMap, Set as makeSet } from 'immutable';
|
||||
|
||||
/**
|
||||
* Returns a cache ID based on the topologyId and optionsQuery
|
||||
@@ -66,14 +67,16 @@ export function updateTopologyIds(topologies, parentId) {
|
||||
// map for easy lookup
|
||||
export function setTopologyUrlsById(topologyUrlsById, topologies) {
|
||||
let urlMap = topologyUrlsById;
|
||||
topologies.forEach(topology => {
|
||||
urlMap = urlMap.set(topology.id, topology.url);
|
||||
if (topology.sub_topologies) {
|
||||
topology.sub_topologies.forEach(subTopology => {
|
||||
urlMap = urlMap.set(subTopology.id, subTopology.url);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (topologies) {
|
||||
topologies.forEach(topology => {
|
||||
urlMap = urlMap.set(topology.id, topology.url);
|
||||
if (topology.sub_topologies) {
|
||||
topology.sub_topologies.forEach(subTopology => {
|
||||
urlMap = urlMap.set(subTopology.id, subTopology.url);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return urlMap;
|
||||
}
|
||||
|
||||
@@ -81,3 +84,56 @@ export function filterHiddenTopologies(topologies) {
|
||||
return topologies.filter(t => (!t.hide_if_empty || t.stats.node_count > 0 ||
|
||||
t.stats.filtered_nodes > 0));
|
||||
}
|
||||
|
||||
export function getActiveTopologyOptions(state) {
|
||||
// options for current topology, sub-topologies share options with parent
|
||||
const parentId = state.getIn(['currentTopology', 'parentId']);
|
||||
if (parentId) {
|
||||
return state.getIn(['topologyOptions', parentId]);
|
||||
}
|
||||
return state.getIn(['topologyOptions', state.get('currentTopologyId')]);
|
||||
}
|
||||
|
||||
export function getCurrentTopologyOptions(state) {
|
||||
return state.getIn(['currentTopology', 'options']);
|
||||
}
|
||||
|
||||
export function isTopologyEmpty(state) {
|
||||
return state.getIn(['currentTopology', 'stats', 'node_count'], 0) === 0
|
||||
&& state.get('nodes').size === 0;
|
||||
}
|
||||
|
||||
export function getAdjacentNodes(state, originNodeId) {
|
||||
let adjacentNodes = makeSet();
|
||||
const nodeId = originNodeId || state.get('selectedNodeId');
|
||||
|
||||
if (nodeId) {
|
||||
if (state.hasIn(['nodes', nodeId])) {
|
||||
adjacentNodes = makeSet(state.getIn(['nodes', nodeId, 'adjacency']));
|
||||
// fill up set with reverse edges
|
||||
state.get('nodes').forEach((node, id) => {
|
||||
if (node.get('adjacency') && node.get('adjacency').includes(nodeId)) {
|
||||
adjacentNodes = adjacentNodes.add(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return adjacentNodes;
|
||||
}
|
||||
|
||||
export function hasSelectedNode(state) {
|
||||
const selectedNodeId = state.get('selectedNodeId');
|
||||
return state.hasIn(['nodes', selectedNodeId]);
|
||||
}
|
||||
|
||||
export function getCurrentTopologyUrl(state) {
|
||||
return state.getIn(['currentTopology', 'url']);
|
||||
}
|
||||
|
||||
export function isSameTopology(nodes, nextNodes) {
|
||||
const mapper = node => makeMap({id: node.get('id'), adjacency: node.get('adjacency')});
|
||||
const topology = nodes.map(mapper);
|
||||
const nextTopology = nextNodes.map(mapper);
|
||||
return isDeepEqual(topology, nextTopology);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import debug from 'debug';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
import { receiveNodesDelta } from '../actions/app-actions';
|
||||
import AppStore from '../stores/app-store';
|
||||
|
||||
const log = debug('scope:update-buffer-utils');
|
||||
const makeList = Immutable.List;
|
||||
@@ -13,8 +12,8 @@ const bufferLength = 100;
|
||||
let deltaBuffer = makeList();
|
||||
let updateTimer = null;
|
||||
|
||||
function isPaused() {
|
||||
return AppStore.isUpdatePaused();
|
||||
function isPaused(getState) {
|
||||
return getState().get('updatePausedAt') !== null;
|
||||
}
|
||||
|
||||
export function resetUpdateBuffer() {
|
||||
@@ -22,8 +21,8 @@ export function resetUpdateBuffer() {
|
||||
deltaBuffer = deltaBuffer.clear();
|
||||
}
|
||||
|
||||
function maybeUpdate() {
|
||||
if (isPaused()) {
|
||||
function maybeUpdate(getState) {
|
||||
if (isPaused(getState)) {
|
||||
clearTimeout(updateTimer);
|
||||
resetUpdateBuffer();
|
||||
} else {
|
||||
@@ -110,6 +109,6 @@ export function getUpdateBufferSize() {
|
||||
return deltaBuffer.size;
|
||||
}
|
||||
|
||||
export function resumeUpdate() {
|
||||
maybeUpdate();
|
||||
export function resumeUpdate(getState) {
|
||||
maybeUpdate(getState);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export function basePathSlash(urlPath) {
|
||||
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const wsUrl = `${wsProto}://${location.host}${basePath(location.pathname)}`;
|
||||
|
||||
function createWebsocket(topologyUrl, optionsQuery) {
|
||||
function createWebsocket(topologyUrl, optionsQuery, dispatch) {
|
||||
if (socket) {
|
||||
socket.onclose = null;
|
||||
socket.onerror = null;
|
||||
@@ -68,67 +68,67 @@ function createWebsocket(topologyUrl, optionsQuery) {
|
||||
socket = new WebSocket(`${wsUrl}${topologyUrl}/ws?t=${updateFrequency}&${optionsQuery}`);
|
||||
|
||||
socket.onopen = () => {
|
||||
openWebsocket();
|
||||
dispatch(openWebsocket());
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
clearTimeout(reconnectTimer);
|
||||
log(`Closing websocket to ${topologyUrl}`, socket.readyState);
|
||||
socket = null;
|
||||
closeWebsocket();
|
||||
dispatch(closeWebsocket());
|
||||
|
||||
reconnectTimer = setTimeout(() => {
|
||||
createWebsocket(topologyUrl, optionsQuery);
|
||||
createWebsocket(topologyUrl, optionsQuery, dispatch);
|
||||
}, reconnectTimerInterval);
|
||||
};
|
||||
|
||||
socket.onerror = () => {
|
||||
log(`Error in websocket to ${topologyUrl}`);
|
||||
receiveError(currentUrl);
|
||||
dispatch(receiveError(currentUrl));
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
receiveNodesDelta(msg);
|
||||
dispatch(receiveNodesDelta(msg));
|
||||
};
|
||||
}
|
||||
|
||||
/* keep URLs relative */
|
||||
|
||||
export function getTopologies(options) {
|
||||
export function getTopologies(options, dispatch) {
|
||||
clearTimeout(topologyTimer);
|
||||
const optionsQuery = buildOptionsQuery(options);
|
||||
const url = `api/topology?${optionsQuery}`;
|
||||
reqwest({
|
||||
url,
|
||||
success: (res) => {
|
||||
receiveTopologies(res);
|
||||
dispatch(receiveTopologies(res));
|
||||
topologyTimer = setTimeout(() => {
|
||||
getTopologies(options);
|
||||
getTopologies(options, dispatch);
|
||||
}, TOPOLOGY_INTERVAL);
|
||||
},
|
||||
error: (err) => {
|
||||
log(`Error in topology request: ${err.responseText}`);
|
||||
receiveError(url);
|
||||
dispatch(receiveError(url));
|
||||
topologyTimer = setTimeout(() => {
|
||||
getTopologies(options);
|
||||
getTopologies(options, dispatch);
|
||||
}, TOPOLOGY_INTERVAL);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getNodesDelta(topologyUrl, options) {
|
||||
export function getNodesDelta(topologyUrl, options, dispatch) {
|
||||
const optionsQuery = buildOptionsQuery(options);
|
||||
|
||||
// only recreate websocket if url changed
|
||||
if (topologyUrl && (topologyUrl !== currentUrl || currentOptions !== optionsQuery)) {
|
||||
createWebsocket(topologyUrl, optionsQuery);
|
||||
createWebsocket(topologyUrl, optionsQuery, dispatch);
|
||||
currentUrl = topologyUrl;
|
||||
currentOptions = optionsQuery;
|
||||
}
|
||||
}
|
||||
|
||||
export function getNodeDetails(topologyUrlsById, nodeMap) {
|
||||
export function getNodeDetails(topologyUrlsById, nodeMap, dispatch) {
|
||||
// get details for all opened nodes
|
||||
const obj = nodeMap.last();
|
||||
if (obj && topologyUrlsById.has(obj.topologyId)) {
|
||||
@@ -140,42 +140,46 @@ export function getNodeDetails(topologyUrlsById, nodeMap) {
|
||||
success: (res) => {
|
||||
// make sure node is still selected
|
||||
if (nodeMap.has(res.node.id)) {
|
||||
receiveNodeDetails(res.node);
|
||||
dispatch(receiveNodeDetails(res.node));
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
log(`Error in node details request: ${err.responseText}`);
|
||||
// dont treat missing node as error
|
||||
if (err.status === 404) {
|
||||
receiveNotFound(obj.id);
|
||||
dispatch(receiveNotFound(obj.id));
|
||||
} else {
|
||||
receiveError(topologyUrl);
|
||||
dispatch(receiveError(topologyUrl));
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
} else if (obj) {
|
||||
log('No details or url found for ', obj);
|
||||
}
|
||||
}
|
||||
|
||||
export function getApiDetails() {
|
||||
export function getApiDetails(dispatch) {
|
||||
clearTimeout(apiDetailsTimer);
|
||||
const url = 'api';
|
||||
reqwest({
|
||||
url,
|
||||
success: (res) => {
|
||||
receiveApiDetails(res);
|
||||
apiDetailsTimer = setTimeout(getApiDetails, API_INTERVAL);
|
||||
dispatch(receiveApiDetails(res));
|
||||
apiDetailsTimer = setTimeout(() => {
|
||||
getApiDetails(dispatch);
|
||||
}, API_INTERVAL);
|
||||
},
|
||||
error: (err) => {
|
||||
log(`Error in api details request: ${err.responseText}`);
|
||||
receiveError(url);
|
||||
apiDetailsTimer = setTimeout(getApiDetails, API_INTERVAL / 2);
|
||||
apiDetailsTimer = setTimeout(() => {
|
||||
getApiDetails(dispatch);
|
||||
}, API_INTERVAL / 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function doControlRequest(nodeId, control) {
|
||||
export function doControlRequest(nodeId, control, dispatch) {
|
||||
clearTimeout(controlErrorTimer);
|
||||
const url = `api/control/${encodeURIComponent(control.probeId)}/`
|
||||
+ `${encodeURIComponent(control.nodeId)}/${control.id}`;
|
||||
@@ -183,26 +187,26 @@ export function doControlRequest(nodeId, control) {
|
||||
method: 'POST',
|
||||
url,
|
||||
success: (res) => {
|
||||
receiveControlSuccess(nodeId);
|
||||
dispatch(receiveControlSuccess(nodeId));
|
||||
if (res) {
|
||||
if (res.pipe) {
|
||||
receiveControlPipe(res.pipe, nodeId, res.raw_tty, true);
|
||||
dispatch(receiveControlPipe(res.pipe, nodeId, res.raw_tty, true));
|
||||
}
|
||||
if (res.removedNode) {
|
||||
receiveControlNodeRemoved(nodeId);
|
||||
dispatch(receiveControlNodeRemoved(nodeId));
|
||||
}
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
receiveControlError(nodeId, err.response);
|
||||
dispatch(receiveControlError(nodeId, err.response));
|
||||
controlErrorTimer = setTimeout(() => {
|
||||
clearControlError(nodeId);
|
||||
dispatch(clearControlError(nodeId));
|
||||
}, 10000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function deletePipe(pipeId) {
|
||||
export function deletePipe(pipeId, dispatch) {
|
||||
const url = `api/pipe/${encodeURIComponent(pipeId)}`;
|
||||
reqwest({
|
||||
method: 'DELETE',
|
||||
@@ -212,12 +216,12 @@ export function deletePipe(pipeId) {
|
||||
},
|
||||
error: (err) => {
|
||||
log(`Error closing pipe:${err}`);
|
||||
receiveError(url);
|
||||
dispatch(receiveError(url));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getPipeStatus(pipeId) {
|
||||
export function getPipeStatus(pipeId, dispatch) {
|
||||
const url = `api/pipe/${encodeURIComponent(pipeId)}/check`;
|
||||
reqwest({
|
||||
method: 'GET',
|
||||
@@ -233,7 +237,7 @@ export function getPipeStatus(pipeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
receiveControlPipeStatus(pipeId, status);
|
||||
dispatch(receiveControlPipeStatus(pipeId, status));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user