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:
David Kaltschmidt
2016-04-27 13:09:00 +02:00
parent 0f3b6bc497
commit 96aae9bc99
50 changed files with 2153 additions and 1921 deletions

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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));
}
});
}