@@ -289,6 +323,5 @@ function mapStateToProps(state) {
export default connect(
- mapStateToProps,
- {setAppState}
+ mapStateToProps
)(DebugToolbar);
diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js
index e9632af3c..20490c321 100644
--- a/client/app/scripts/components/nodes.js
+++ b/client/app/scripts/components/nodes.js
@@ -13,24 +13,6 @@ const navbarHeight = 194;
const marginTop = 0;
-/**
- * dynamic coords precision based on topology size
- */
-function getLayoutPrecision(nodesCount) {
- let precision;
- if (nodesCount >= 50) {
- precision = 0;
- } else if (nodesCount > 20) {
- precision = 1;
- } else if (nodesCount > 10) {
- precision = 2;
- } else {
- precision = 3;
- }
-
- return precision;
-}
-
class Nodes extends React.Component {
constructor(props, context) {
super(props, context);
@@ -67,9 +49,8 @@ class Nodes extends React.Component {
}
render() {
- const { nodes, topologyEmpty, gridMode, topologiesLoaded, nodesLoaded, topologies,
+ const { topologyEmpty, gridMode, topologiesLoaded, nodesLoaded, topologies,
currentTopology } = this.props;
- const layoutPrecision = getLayoutPrecision(nodes.size);
return (
@@ -84,13 +65,10 @@ class Nodes extends React.Component {
{gridMode ?
:
}
);
@@ -113,7 +91,6 @@ function mapStateToProps(state) {
return {
currentTopology: state.get('currentTopology'),
gridMode: state.get('gridMode'),
- nodes: state.get('nodes').filter(node => !node.get('filtered')),
nodesLoaded: state.get('nodesLoaded'),
topologies: state.get('topologies'),
topologiesLoaded: state.get('topologiesLoaded'),
diff --git a/client/app/scripts/selectors/chartSelectors.js b/client/app/scripts/selectors/chartSelectors.js
new file mode 100644
index 000000000..5792a56b2
--- /dev/null
+++ b/client/app/scripts/selectors/chartSelectors.js
@@ -0,0 +1,118 @@
+import debug from 'debug';
+import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';
+import { Map as makeMap, is, Set } from 'immutable';
+
+import { getAdjacentNodes } from '../utils/topology-utils';
+
+
+const log = debug('scope:selectors');
+
+
+//
+// `mergeDeepKeyIntersection` does a deep merge on keys that exists in both maps
+//
+function mergeDeepKeyIntersection(mapA, mapB) {
+ const commonKeys = Set.fromKeys(mapA).intersect(mapB.keySeq());
+ return makeMap(commonKeys.map(k => [k, mapA.get(k).mergeDeep(mapB.get(k))]));
+}
+
+
+//
+// `returnPreviousRefIfEqual` is a helper function that checks the new computed of a selector
+// against the previously computed value. If they are deeply equal return the previous result. This
+// is important for things like connect() which tests whether componentWillReceiveProps should be
+// called by doing a '===' on the values you return from mapStateToProps.
+//
+// e.g.
+//
+// const filteredThings = createSelector(
+// state => state.things,
+// (things) => things.filter(t => t > 2)
+// );
+//
+// // This will trigger componentWillReceiveProps on every store change:
+// connect(s => { things: filteredThings(s) }, ThingComponent);
+//
+// // But if we wrap it, the result will be === if it `is()` equal and...
+// const filteredThingsWrapped = returnPreviousRefIfEqual(filteredThings);
+//
+// // ...We're safe!
+// connect(s => { things: filteredThingsWrapped(s) }, ThingComponent);
+//
+// Note: This is a slightly strange way to use reselect. Selectors memoize their *arguments* not
+// "their results", so use the result of the wrapped selector as the argument to another selector
+// here to memoize it and get what we want.
+//
+const _createDeepEqualSelector = createSelectorCreator(defaultMemoize, is);
+const _identity = v => v;
+const returnPreviousRefIfEqual = (selector) => _createDeepEqualSelector(selector, _identity);
+
+
+//
+// Selectors!
+//
+
+
+const allNodesSelector = state => state.get('nodes');
+
+
+export const nodesSelector = returnPreviousRefIfEqual(
+ createSelector(
+ allNodesSelector,
+ (allNodes) => allNodes.filter(node => !node.get('filtered'))
+ )
+);
+
+
+export const adjacentNodesSelector = returnPreviousRefIfEqual(getAdjacentNodes);
+
+
+export const nodeAdjacenciesSelector = returnPreviousRefIfEqual(
+ createSelector(
+ nodesSelector,
+ (nodes) => nodes.map(n => makeMap({
+ id: n.get('id'),
+ adjacency: n.get('adjacency'),
+ }))
+ )
+);
+
+
+export const dataNodesSelector = createSelector(
+ nodesSelector,
+ (nodes) => nodes.map((node, id) => makeMap({
+ id,
+ label: node.get('label'),
+ pseudo: node.get('pseudo'),
+ subLabel: node.get('label_minor'),
+ nodeCount: node.get('node_count'),
+ metrics: node.get('metrics'),
+ rank: node.get('rank'),
+ shape: node.get('shape'),
+ stack: node.get('stack'),
+ networks: node.get('networks'),
+ }))
+);
+
+
+//
+// FIXME: this is a bit of a hack...
+//
+export const layoutNodesSelector = (_, props) => props.layoutNodes || makeMap();
+
+
+export const completeNodesSelector = createSelector(
+ layoutNodesSelector,
+ dataNodesSelector,
+ (layoutNodes, dataNodes) => {
+ //
+ // There are no guarantees whether this selector will be computed first (when
+ // node-chart-elements.mapStateToProps is called by store.subscribe before
+ // nodes-chart.mapStateToProps is called), and component render batching and yadada.
+ //
+ if (layoutNodes.size !== dataNodes.size) {
+ log('Obviously mismatched node data', layoutNodes.size, dataNodes.size);
+ }
+ return mergeDeepKeyIntersection(dataNodes, layoutNodes);
+ }
+);
diff --git a/client/app/scripts/utils/topology-utils.js b/client/app/scripts/utils/topology-utils.js
index ae22afaba..f1a9f1b5c 100644
--- a/client/app/scripts/utils/topology-utils.js
+++ b/client/app/scripts/utils/topology-utils.js
@@ -1,5 +1,5 @@
import _ from 'lodash';
-import { is as isDeepEqual, Map as makeMap, Set as makeSet, List as makeList } from 'immutable';
+import { Set as makeSet, List as makeList } from 'immutable';
//
@@ -143,6 +143,7 @@ export function isTopologyEmpty(state) {
&& state.get('nodes').size === 0;
}
+
export function getAdjacentNodes(state, originNodeId) {
let adjacentNodes = makeSet();
const nodeId = originNodeId || state.get('selectedNodeId');
@@ -171,13 +172,6 @@ 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);
-}
-
export function isNodeMatchingQuery(node, query) {
return node.get('label').includes(query) || node.get('subLabel').includes(query);
}
diff --git a/client/package.json b/client/package.json
index f2d2a888e..207f383e1 100644
--- a/client/package.json
+++ b/client/package.json
@@ -29,6 +29,7 @@
"redux-logger": "2.6.1",
"redux-thunk": "2.0.1",
"reqwest": "~2.0.5",
+ "reselect": "^2.5.3",
"timely": "0.1.0",
"whatwg-fetch": "0.11.0"
},
diff --git a/client/server.js b/client/server.js
index a27ac30c7..3b984ba1f 100644
--- a/client/server.js
+++ b/client/server.js
@@ -66,7 +66,7 @@ if (process.env.NODE_ENV !== 'production') {
hot: true,
noInfo: true,
historyApiFallback: true,
- stats: { colors: true }
+ stats: 'errors-only',
}).listen(4041, '0.0.0.0', function (err, result) {
if (err) {
console.log(err);
diff --git a/client/test/actions/90-nodes-select.js b/client/test/actions/90-nodes-select.js
index ed7c28313..5693ae01b 100644
--- a/client/test/actions/90-nodes-select.js
+++ b/client/test/actions/90-nodes-select.js
@@ -18,85 +18,84 @@ function clickIfVisible(list, index) {
});
}
+
+function selectNode(browser) {
+ debug('select node');
+ return browser.elementByCssSelector('.nodes-chart-nodes .node:nth-child(1) > g', function(err, el) {
+ return el.click();
+ });
+}
+
+
+function deselectNode(browser) {
+ debug('deselect node');
+ return browser.elementByCssSelector('.fa-close', function(err, el) {
+ return el.click();
+ });
+}
+
+
module.exports = function(cfg) {
- var startUrl = 'http://' + cfg.host + '/debug.html';
- var selectedUrl = 'http://' + cfg.host + '/debug.html#!/state/{"nodeDetails":[{"id":"zing11","label":"zing11","topologyId":"containers"}],"selectedNodeId":"zing11","topologyId":"containers","topologyOptions":{"processes":{"unconnected":"hide"},"processes-by-name":{"unconnected":"hide"},"containers":{"system":"hide","stopped":"hide"},"containers-by-hostname":{"system":"hide","stopped":"hide"},"containers-by-image":{"system":"hide","stopped":"hide"}}}';
-
+ var startUrl = 'http://' + cfg.host + '/';
// cfg - The configuration object. args, from the example above.
return function(browser) {
// browser is created using wd.promiseRemote()
// More info about wd at https://github.com/admc/wd
- return browser.get('http://' + cfg.host + '/debug.html')
+ return browser.get('http://' + cfg.host + '/')
.then(function() {
debug('starting run ' + cfg.run);
return browser.sleep(2000);
})
+ .then(function() {
+ return browser.execute("localStorage.debugToolbar = 1;");
+ })
+ .then(function() {
+ return browser.sleep(5000);
+ })
.then(function() {
return browser.elementByCssSelector('.debug-panel button:nth-child(5)');
// return browser.elementByCssSelector('.debug-panel div:nth-child(2) button:nth-child(9)');
})
.then(function(el) {
debug('debug-panel found');
- return el.click(function() {
- el.click(function() {
- el.click();
- });
- });
+ return el.click();
})
.then(function() {
return browser.sleep(2000);
})
-
.then(function() {
- return browser.sleep(2000);
- })
- .then(function() {
- debug('select node');
- return browser.get(selectedUrl);
+ return selectNode(browser);
})
.then(function() {
return browser.sleep(5000);
})
.then(function() {
- debug('deselect node');
- return browser.elementByCssSelector('.fa-close', function(err, el) {
- return el.click();
- });
+ return deselectNode(browser);
})
-
.then(function() {
return browser.sleep(2000);
})
.then(function() {
- debug('select node');
- return browser.get(selectedUrl);
+ return selectNode(browser);
})
.then(function() {
return browser.sleep(5000);
})
.then(function() {
- debug('deselect node');
- return browser.elementByCssSelector('.fa-close', function(err, el) {
- return el.click();
- });
+ return deselectNode(browser);
})
-
.then(function() {
return browser.sleep(2000);
})
.then(function() {
- debug('select node');
- return browser.get(selectedUrl);
+ return selectNode(browser);
})
.then(function() {
return browser.sleep(5000);
})
.then(function() {
- debug('deselect node');
- return browser.elementByCssSelector('.fa-close', function(err, el) {
- return el.click();
- });
+ return deselectNode(browser);
})
.then(function() {
diff --git a/client/test/browser-perf/main.js b/client/test/browser-perf/main.js
index 039c6d3ad..8c042556e 100644
--- a/client/test/browser-perf/main.js
+++ b/client/test/browser-perf/main.js
@@ -3,7 +3,7 @@ var options = {
selenium: 'http://local.docker:4444/wd/hub',
actions: [require('./custom-action.js')()]
}
-browserPerf('http://local.docker:4040/debug.html', function(err, res){
+browserPerf('http://local.docker:4040/dev.html', function(err, res){
console.error(err);
console.log(res);
}, options);
diff --git a/client/test/run-jankie.sh b/client/test/run-jankie.sh
index 2c3f16b2c..005c8444a 100755
--- a/client/test/run-jankie.sh
+++ b/client/test/run-jankie.sh
@@ -10,6 +10,12 @@
#
# perfjankie --only-update-site --couch-server http://local.docker:5984 --couch-database performance
#
+# Optional: run from localhost which can be a bit fast than rebuilding...
+# - ssh -R 0.0.0.0:4042:localhost:4042 docker@local.docker
+# - npm run build
+# - BACKEND_HOST=local.docker npm start
+# - ./run-jankie.sh 192.168.64.3:4042
+#
# Usage:
#
# ./run-jankie.sh 192.168.64.3:4040
@@ -26,9 +32,9 @@ COMMIT=$(git log --format="%h" -1)
echo "Testing $COMMIT on $DATE"
-../../scope stop
-make SUDO= -C ../..
-../../scope launch
-sleep 5
+# ../../scope stop
+# make SUDO= -C ../..
+# ../../scope launch
+# sleep 5
COMMIT="$COMMIT" DATE=$DATE HOST=$HOST DEBUG=scope* node ./perfjankie/main.js