diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 7fba88d65..713ff60a6 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -57,6 +57,12 @@ export function clickCloseTerminal(pipeId, closePipe) { updateRoute(); } +export function clickForceRelayout() { + AppDispatcher.dispatch({ + type: ActionTypes.CLICK_FORCE_RELAYOUT + }); +} + export function clickNode(nodeId, label, origin) { AppDispatcher.dispatch({ type: ActionTypes.CLICK_NODE, diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 91030f316..4f16b1879 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -74,7 +74,7 @@ export default class NodesChart extends React.Component { }); } // FIXME add PureRenderMixin, Immutables, and move the following functions to render() - if (nextProps.nodes !== this.props.nodes) { + if (nextProps.forceRelayout || nextProps.nodes !== this.props.nodes) { _.assign(state, this.updateGraphState(nextProps, state)); } if (this.props.selectedNodeId !== nextProps.selectedNodeId) { @@ -411,6 +411,7 @@ export default class NodesChart extends React.Component { height: props.height, scale: nodeScale, margins: MARGINS, + forceRelayout: props.forceRelayout, topologyId: this.props.topologyId }; diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index ccad2042a..05daf643c 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -361,7 +361,7 @@ export function doLayout(immNodes, immEdges, opts) { let layout; ++layoutRuns; - if (cachedLayout && nodeCache && edgeCache && !hasUnseenNodes(immNodes, nodeCache)) { + if (!options.forceRelayout && cachedLayout && nodeCache && edgeCache && !hasUnseenNodes(immNodes, nodeCache)) { log('skip layout, trivial adjustment', ++layoutRunsTrivial, layoutRuns); layout = cloneLayout(cachedLayout, immNodes, immEdges); // copy old properties, works also if nodes get re-added diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 7b57180e6..9a796b5a1 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -7,7 +7,7 @@ import Status from './status.js'; import Topologies from './topologies.js'; import TopologyOptions from './topology-options.js'; import { getApiDetails, getTopologies, basePathSlash } from '../utils/web-api-utils'; -import { hitEsc } from '../actions/app-actions'; +import { clickForceRelayout, hitEsc } from '../actions/app-actions'; import Details from './details'; import Nodes from './nodes'; import EmbeddedTerminal from './embedded-terminal'; @@ -25,6 +25,7 @@ function getStateFromStores() { currentTopologyId: AppStore.getCurrentTopologyId(), currentTopologyOptions: AppStore.getCurrentTopologyOptions(), errorUrl: AppStore.getErrorUrl(), + forceRelayout: AppStore.isForceRelayout(), highlightedEdgeIds: AppStore.getHighlightedEdgeIds(), highlightedNodeIds: AppStore.getHighlightedNodeIds(), hostname: AppStore.getHostname(), @@ -79,6 +80,8 @@ export default class App extends React.Component { // link url to switch contrast with current UI state const otherContrastModeUrl = contrastMode ? basePathSlash(window.location.pathname) : 'contrast.html'; const otherContrastModeTitle = contrastMode ? 'Switch to normal contrast' : 'Switch to high contrast'; + const forceRelayoutClassName = 'footer-label footer-label-icon'; + const forceRelayoutTitle = 'Force re-layout (might reduce edge crossings, but may shift nodes around)'; return (
@@ -100,6 +103,7 @@ export default class App extends React.Component { @@ -117,6 +121,9 @@ export default class App extends React.Component { on {this.state.hostname}   + + + diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js index 291d27732..42e9aaf16 100644 --- a/client/app/scripts/components/nodes.js +++ b/client/app/scripts/components/nodes.js @@ -33,6 +33,7 @@ export default class Nodes extends React.Component { nodes={this.props.nodes} width={this.state.width} height={this.state.height} + forceRelayout={this.props.forceRelayout} topologyId={this.props.topologyId} detailsWidth={this.props.detailsWidth} topMargin={this.props.topMargin} diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index 0cdf0b39a..3e62b7bc2 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -6,6 +6,7 @@ const ACTION_TYPES = [ 'CLICK_BACKGROUND', 'CLICK_CLOSE_DETAILS', 'CLICK_CLOSE_TERMINAL', + 'CLICK_FORCE_RELAYOUT', 'CLICK_NODE', 'CLICK_RELATIVE', 'CLICK_SHOW_TOPOLOGY_FOR_NODE', diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index fa6b4036d..1b6d8056e 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -56,6 +56,7 @@ let controlStatus = makeMap(); let currentTopology = null; let currentTopologyId = 'containers'; let errorUrl = null; +let forceRelayout = false; let hostname = '...'; let version = '...'; let mouseOverEdgeId = null; @@ -266,6 +267,10 @@ export class AppStore extends Store { return version; } + isForceRelayout() { + return forceRelayout; + } + isRouteSet() { return routeSet; } @@ -320,6 +325,15 @@ export class AppStore extends Store { this.__emitChange(); break; + case ActionTypes.CLICK_FORCE_RELAYOUT: + forceRelayout = true; + // fire only once, reset after emitChange + setTimeout(() => { + forceRelayout = false; + }, 0); + this.__emitChange(); + break; + case ActionTypes.CLICK_NODE: const prevSelectedNodeId = selectedNodeId; const prevDetailsStackSize = nodeDetails.size; @@ -482,6 +496,8 @@ export class AppStore extends Store { case ActionTypes.RECEIVE_NODES_DELTA: const emptyMessage = !payload.delta.add && !payload.delta.remove && !payload.delta.update; + // this action is called frequently, good to check if something changed + const emitChange = !emptyMessage || errorUrl !== null; if (!emptyMessage) { log('RECEIVE_NODES_DELTA', @@ -516,7 +532,9 @@ export class AppStore extends Store { nodes = nodes.set(node.id, Immutable.fromJS(makeNode(node))); }); - this.__emitChange(); + if (emitChange) { + this.__emitChange(); + } break; case ActionTypes.RECEIVE_NOT_FOUND: