Force-relayout button

* new button in footer
* when clicked, forces a relayout that could help with degraded graphs
* sets a store flag that will be unset on next nodes delta update
* fixes #863
This commit is contained in:
David Kaltschmidt
2016-02-18 13:06:27 +01:00
parent 1f5aaa28ee
commit 9dafaef9a9
7 changed files with 38 additions and 4 deletions

View File

@@ -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,

View File

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

View File

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

View File

@@ -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 (
<div className="app">
@@ -100,6 +103,7 @@ export default class App extends React.Component {
<Nodes nodes={this.state.nodes} highlightedNodeIds={this.state.highlightedNodeIds}
highlightedEdgeIds={this.state.highlightedEdgeIds} detailsWidth={detailsWidth}
selectedNodeId={this.state.selectedNodeId} topMargin={topMargin}
forceRelayout={this.state.forceRelayout}
topologyId={this.state.currentTopologyId} />
<Sidebar>
@@ -117,6 +121,9 @@ export default class App extends React.Component {
<span className="footer-label">on</span>
{this.state.hostname}
&nbsp;
<a className={forceRelayoutClassName} onClick={clickForceRelayout} title={forceRelayoutTitle}>
<span className="fa fa-refresh" />
</a>
<a className="footer-label footer-label-icon" href={otherContrastModeUrl} title={otherContrastModeTitle}>
<span className="fa fa-adjust" />
</a>

View File

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

View File

@@ -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',

View File

@@ -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: