diff --git a/client/app/scripts/charts/nodes-chart-elements.js b/client/app/scripts/charts/nodes-chart-elements.js index 7d921f0a8..19b1aa996 100644 --- a/client/app/scripts/charts/nodes-chart-elements.js +++ b/client/app/scripts/charts/nodes-chart-elements.js @@ -1,6 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; +import { completeNodesSelector } from '../selectors/chartSelectors'; import NodesChartEdges from './nodes-chart-edges'; import NodesChartNodes from './nodes-chart-nodes'; @@ -9,14 +10,24 @@ class NodesChartElements extends React.Component { const props = this.props; return ( - - ); } } -export default connect()(NodesChartElements); +function mapStateToProps(state, props) { + return { + completeNodes: completeNodesSelector(state, props) + }; +} + +export default connect(mapStateToProps)(NodesChartElements); diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index e42e239a0..4a37f2af5 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -3,17 +3,17 @@ import d3 from 'd3'; import debug from 'debug'; import React from 'react'; import { connect } from 'react-redux'; -import { Map as makeMap, fromJS, is as isDeepEqual } from 'immutable'; +import { Map as makeMap, fromJS } from 'immutable'; import timely from 'timely'; +import { nodeAdjacenciesSelector, adjacentNodesSelector } from '../selectors/chartSelectors'; import { clickBackground } from '../actions/app-actions'; import { EDGE_ID_SEPARATOR } from '../constants/naming'; import { MIN_NODE_SIZE, DETAILS_PANEL_WIDTH, MAX_NODE_SIZE } from '../constants/styles'; import Logo from '../components/logo'; import { doLayout } from './nodes-layout'; import NodesChartElements from './nodes-chart-elements'; -import { getActiveTopologyOptions, getAdjacentNodes, - isSameTopology } from '../utils/topology-utils'; +import { getActiveTopologyOptions } from '../utils/topology-utils'; const log = debug('scope:nodes-chart'); @@ -24,6 +24,94 @@ const radiusDensity = d3.scale.threshold() .domain([3, 6]) .range([2.5, 3.5, 3]); +/** + * 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; +} + + +function initEdges(nodes) { + let edges = makeMap(); + + nodes.forEach((node, nodeId) => { + const adjacency = node.get('adjacency'); + if (adjacency) { + adjacency.forEach(adjacent => { + const edge = [nodeId, adjacent]; + const edgeId = edge.join(EDGE_ID_SEPARATOR); + + if (!edges.has(edgeId)) { + const source = edge[0]; + const target = edge[1]; + if (nodes.has(source) && nodes.has(target)) { + edges = edges.set(edgeId, makeMap({ + id: edgeId, + value: 1, + source, + target + })); + } + } + }); + } + }); + + return edges; +} + + +function getNodeScale(nodesCount, width, height) { + const expanse = Math.min(height, width); + const nodeSize = expanse / 3; // single node should fill a third of the screen + const maxNodeSize = Math.min(MAX_NODE_SIZE, expanse / 10); + const normalizedNodeSize = Math.max(MIN_NODE_SIZE, + Math.min(nodeSize / Math.sqrt(nodesCount), maxNodeSize)); + + return d3.scale.linear().range([0, normalizedNodeSize]); +} + + +function updateLayout(width, height, nodes, baseOptions) { + const nodeScale = getNodeScale(nodes.size, width, height); + const edges = initEdges(nodes); + + const options = Object.assign({}, baseOptions, { + scale: nodeScale, + }); + + const timedLayouter = timely(doLayout); + const graph = timedLayouter(nodes, edges, options); + + log(`graph layout took ${timedLayouter.time}ms`); + + const layoutNodes = graph.nodes.map(node => makeMap({ + x: node.get('x'), + y: node.get('y'), + // extract coords and save for restore + px: node.get('x'), + py: node.get('y') + })); + + const layoutEdges = graph.edges + .map(edge => edge.set('ppoints', edge.get('points'))); + + return { layoutNodes, layoutEdges, layoutWidth: graph.width, layoutHeight: graph.height }; +} + + class NodesChart extends React.Component { constructor(props, context) { @@ -43,7 +131,7 @@ class NodesChart extends React.Component { hasZoomed: false, height: props.height || 0, width: props.width || 0, - zoomCache: {} + zoomCache: {}, }; } @@ -82,8 +170,7 @@ class NodesChart extends React.Component { state.height = nextProps.forceRelayout ? nextProps.height : (state.height || nextProps.height); state.width = nextProps.forceRelayout ? nextProps.width : (state.width || nextProps.width); - // _.assign(state, this.updateGraphState(nextProps, state)); - if (nextProps.forceRelayout || !isSameTopology(nextProps.nodes, this.props.nodes)) { + if (nextProps.forceRelayout || nextProps.nodes !== this.props.nodes) { _.assign(state, this.updateGraphState(nextProps, state)); } @@ -127,6 +214,7 @@ class NodesChart extends React.Component { const transform = `translate(${translate}) scale(${scale})`; const svgClassNames = this.props.isEmpty ? 'hide' : ''; + const layoutPrecision = getLayoutPrecision(nodes.size); return (
- + layoutPrecision={layoutPrecision} />
); @@ -151,70 +243,10 @@ class NodesChart extends React.Component { } } - initNodes(topology, stateNodes) { - let nextStateNodes = stateNodes; - - // remove nodes that have disappeared - stateNodes.forEach((node, id) => { - if (!topology.has(id)) { - nextStateNodes = nextStateNodes.delete(id); - } - }); - - // copy relevant fields to state nodes - topology.forEach((node, id) => { - nextStateNodes = nextStateNodes.mergeIn([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'), - })); - }); - - return nextStateNodes; - } - - initEdges(topology, stateNodes) { - let edges = makeMap(); - - topology.forEach((node, nodeId) => { - const adjacency = node.get('adjacency'); - if (adjacency) { - adjacency.forEach(adjacent => { - const edge = [nodeId, adjacent]; - const edgeId = edge.join(EDGE_ID_SEPARATOR); - - if (!edges.has(edgeId)) { - const source = edge[0]; - const target = edge[1]; - if (stateNodes.has(source) && stateNodes.has(target)) { - edges = edges.set(edgeId, makeMap({ - id: edgeId, - value: 1, - source, - target - })); - } - } - }); - } - }); - - return edges; - } - centerSelectedNode(props, state) { let stateNodes = state.nodes; let stateEdges = state.edges; - const selectedLayoutNode = stateNodes.get(props.selectedNodeId); - - if (!selectedLayoutNode) { + if (!stateNodes.has(props.selectedNodeId)) { return {}; } @@ -245,8 +277,8 @@ class NodesChart extends React.Component { const radius = Math.min(state.width, state.height) / density / zoomScale; const offsetAngle = Math.PI / 4; - stateNodes = stateNodes.map((node) => { - const index = adjacentLayoutNodeIds.indexOf(node.get('id')); + stateNodes = stateNodes.map((node, nodeId) => { + const index = adjacentLayoutNodeIds.indexOf(nodeId); if (index > -1) { const angle = offsetAngle + Math.PI * 2 * index / adjacentCount; return node.merge({ @@ -259,8 +291,8 @@ class NodesChart extends React.Component { // fix all edges for circular nodes stateEdges = stateEdges.map(edge => { - if (edge.get('source') === selectedLayoutNode.get('id') - || edge.get('target') === selectedLayoutNode.get('id') + if (edge.get('source') === props.selectedNodeId + || edge.get('target') === props.selectedNodeId || _.includes(adjacentLayoutNodeIds, edge.get('source')) || _.includes(adjacentLayoutNodeIds, edge.get('target'))) { const source = stateNodes.get(edge.get('source')); @@ -274,7 +306,7 @@ class NodesChart extends React.Component { }); // auto-scale node size for selected nodes - const selectedNodeScale = this.getNodeScale(adjacentNodes, state.width, state.height); + const selectedNodeScale = getNodeScale(adjacentNodes.size, state.width, state.height); return { selectedNodeScale, @@ -311,44 +343,23 @@ class NodesChart extends React.Component { }; } - const stateNodes = this.initNodes(props.nodes, state.nodes); - const stateEdges = this.initEdges(props.nodes, stateNodes); - const nodeScale = this.getNodeScale(props.nodes, state.width, state.height); - const nextState = { nodeScale }; - const options = { width: state.width, height: state.height, - scale: nodeScale, margins: props.margins, forceRelayout: props.forceRelayout, - topologyId: this.props.topologyId, - topologyOptions: this.props.topologyOptions + topologyId: props.topologyId, + topologyOptions: props.topologyOptions, }; - const timedLayouter = timely(doLayout); - const graph = timedLayouter(stateNodes, stateEdges, options); - - log(`graph layout took ${timedLayouter.time}ms`); - - // extract coords and save for restore - const graphNodes = graph.nodes.map(node => makeMap({ - x: node.get('x'), - px: node.get('x'), - y: node.get('y'), - py: node.get('y') - })); - - const layoutNodes = stateNodes.mergeDeep(graphNodes); - - const layoutEdges = graph.edges - .map(edge => edge.set('ppoints', edge.get('points'))); - + const { layoutNodes, layoutEdges, layoutWidth, layoutHeight } = updateLayout( + state.width, state.height, props.nodes, options); + // // adjust layout based on viewport - const xFactor = (state.width - props.margins.left - props.margins.right) / graph.width; - const yFactor = state.height / graph.height; + const xFactor = (state.width - props.margins.left - props.margins.right) / layoutWidth; + const yFactor = state.height / layoutHeight; const zoomFactor = Math.min(xFactor, yFactor); - let zoomScale = this.state.scale; + let zoomScale = state.scale; if (this.zoom && !state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) { zoomScale = zoomFactor; @@ -356,24 +367,12 @@ class NodesChart extends React.Component { this.zoom.scale(zoomFactor); } - nextState.scale = zoomScale; - if (!isDeepEqual(layoutNodes, state.nodes)) { - nextState.nodes = layoutNodes; - } - if (!isDeepEqual(layoutEdges, state.edges)) { - nextState.edges = layoutEdges; - } - - return nextState; - } - - getNodeScale(nodes, width, height) { - const expanse = Math.min(height, width); - const nodeSize = expanse / 3; // single node should fill a third of the screen - const maxNodeSize = Math.min(MAX_NODE_SIZE, expanse / 10); - const normalizedNodeSize = Math.max(MIN_NODE_SIZE, - Math.min(nodeSize / Math.sqrt(nodes.size), maxNodeSize)); - return this.state.nodeScale.copy().range([0, normalizedNodeSize]); + return { + scale: zoomScale, + nodes: layoutNodes, + edges: layoutEdges, + nodeScale: getNodeScale(props.nodes.size, state.width, state.height), + }; } zoomed() { @@ -391,9 +390,11 @@ class NodesChart extends React.Component { } } + function mapStateToProps(state) { return { - adjacentNodes: getAdjacentNodes(state), + nodes: nodeAdjacenciesSelector(state), + adjacentNodes: adjacentNodesSelector(state), forceRelayout: state.get('forceRelayout'), selectedNodeId: state.get('selectedNodeId'), topologyId: state.get('currentTopologyId'), @@ -401,6 +402,7 @@ function mapStateToProps(state) { }; } + export default connect( mapStateToProps, { clickBackground } diff --git a/client/app/scripts/charts/nodes-grid.js b/client/app/scripts/charts/nodes-grid.js index e05172f7d..6456c0224 100644 --- a/client/app/scripts/charts/nodes-grid.js +++ b/client/app/scripts/charts/nodes-grid.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import { List as makeList, Map as makeMap } from 'immutable'; import NodeDetailsTable from '../components/node-details/node-details-table'; import { clickNode, sortOrderChanged } from '../actions/app-actions'; +import { nodesSelector } from '../selectors/chartSelectors'; import { getNodeColor } from '../utils/color-utils'; @@ -142,6 +143,7 @@ class NodesGrid extends React.Component { function mapStateToProps(state) { return { + nodes: nodesSelector(state), gridSortBy: state.get('gridSortBy'), gridSortedDesc: state.get('gridSortedDesc'), currentTopology: state.get('currentTopology'), diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index b324ce059..3621f64e9 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -142,6 +142,7 @@ class App extends React.Component { } } + function mapStateToProps(state) { return { activeTopologyOptions: getActiveTopologyOptions(state), @@ -158,6 +159,7 @@ function mapStateToProps(state) { }; } + export default connect( mapStateToProps )(App); diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index ae61fff59..369238d1e 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -43,9 +43,6 @@ const LABEL_PREFIXES = _.range('A'.charCodeAt(), 'Z'.charCodeAt() + 1) .map(n => String.fromCharCode(n)); -// const randomLetter = () => _.sample(LABEL_PREFIXES); - - const deltaAdd = ( name, adjacency = [], shape = 'circle', stack = false, nodeCount = 1, networks = NETWORKS @@ -71,7 +68,7 @@ function addMetrics(availableMetrics, node, v) { ]); return Object.assign({}, node, { - metrics: metrics.map(m => Object.assign({}, m, {max: 100, value: v})) + metrics: metrics.map(m => Object.assign({}, m, {label: 'zing', max: 100, value: v})).toJS() }); } @@ -94,14 +91,16 @@ function addAllVariants(dispatch) { } -function addAllMetricVariants(availableMetrics, dispatch) { +function addAllMetricVariants(availableMetrics) { const newNodes = _.flattenDeep(METRIC_FILLS.map((v, i) => ( SHAPES.map(s => [addMetrics(availableMetrics, deltaAdd(label(s) + i, [], s), v)]) ))); - dispatch(receiveNodesDelta({ - add: newNodes - })); + return (dispatch) => { + dispatch(receiveNodesDelta({ + add: newNodes + })); + }; } @@ -177,11 +176,28 @@ class DebugToolbar extends React.Component { }); } - setLoading(loading) { - this.props.setAppState(state => state.set('topologiesLoaded', !loading)); + asyncDispatch(v) { + setTimeout(() => this.props.dispatch(v), 0); } - addNodes(n, prefix = 'zing') { + setLoading(loading) { + this.asyncDispatch(setAppState(state => state.set('topologiesLoaded', !loading))); + } + + updateAdjacencies() { + const ns = this.props.nodes; + const nodeNames = ns.keySeq().toJS(); + this.asyncDispatch(receiveNodesDelta({ + add: this._addNodes(7), + update: sample(nodeNames).map(n => ({ + id: n, + adjacency: sample(nodeNames), + }), nodeNames.length), + remove: this._removeNode(), + })); + } + + _addNodes(n, prefix = 'zing') { const ns = this.props.nodes; const nodeNames = ns.keySeq().toJS(); const newNodeNames = _.range(ns.size, ns.size + n).map(i => ( @@ -189,19 +205,35 @@ class DebugToolbar extends React.Component { `${prefix}${i}` )); const allNodes = _(nodeNames).concat(newNodeNames).value(); + return newNodeNames.map((name) => deltaAdd( + name, + sample(allNodes), + _.sample(SHAPES), + _.sample(STACK_VARIANTS), + _.sample(NODE_COUNTS), + sample(NETWORKS, 10) + )); + } - this.props.dispatch(receiveNodesDelta({ - add: newNodeNames.map((name) => deltaAdd( - name, - sample(allNodes), - _.sample(SHAPES), - _.sample(STACK_VARIANTS), - _.sample(NODE_COUNTS), - sample(NETWORKS, 10) - )) + addNodes(n, prefix = 'zing') { + setTimeout(() => { + this.asyncDispatch(receiveNodesDelta({ + add: this._addNodes(n, prefix) + })); + log('added nodes', n); + }, 0); + } + + _removeNode() { + const ns = this.props.nodes; + const nodeNames = ns.keySeq().toJS(); + return [nodeNames[_.random(nodeNames.length - 1)]]; + } + + removeNode() { + this.asyncDispatch(receiveNodesDelta({ + remove: this._removeNode() })); - - log('added nodes', n); } render() { @@ -215,11 +247,13 @@ class DebugToolbar extends React.Component { - - + + +
@@ -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