Merge pull request #712 from weaveworks/es2015

JS to ES2015
This commit is contained in:
Simon
2015-12-01 16:45:56 +01:00
37 changed files with 869 additions and 945 deletions

0
client/.eslintignore Normal file
View File

View File

@@ -1,207 +1,197 @@
let ActionTypes;
let AppDispatcher;
let AppStore;
let RouterUtils;
let WebapiUtils;
import AppDispatcher from '../dispatcher/app-dispatcher';
import ActionTypes from '../constants/action-types';
module.exports = {
changeTopologyOption: function(option, value, topologyId) {
AppDispatcher.dispatch({
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
topologyId: topologyId,
option: option,
value: value
});
RouterUtils.updateRoute();
// update all request workers with new options
WebapiUtils.getTopologies(
AppStore.getActiveTopologyOptions()
);
WebapiUtils.getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getActiveTopologyOptions()
);
WebapiUtils.getNodeDetails(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId()
);
},
import { updateRoute } from '../utils/router-utils';
import { doControl as doControlRequest, getNodesDelta, getNodeDetails, getTopologies } from '../utils/web-api-utils';
import AppStore from '../stores/app-store';
clickCloseDetails: function() {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_CLOSE_DETAILS
});
RouterUtils.updateRoute();
},
export function changeTopologyOption(option, value, topologyId) {
AppDispatcher.dispatch({
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
topologyId: topologyId,
option: option,
value: value
});
updateRoute();
// update all request workers with new options
getTopologies(
AppStore.getActiveTopologyOptions()
);
getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getActiveTopologyOptions()
);
getNodeDetails(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId()
);
}
clickNode: function(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_NODE,
nodeId: nodeId
});
RouterUtils.updateRoute();
WebapiUtils.getNodeDetails(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId()
);
},
export function clickCloseDetails() {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_CLOSE_DETAILS
});
updateRoute();
}
clickTopology: function(topologyId) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_TOPOLOGY,
topologyId: topologyId
});
RouterUtils.updateRoute();
WebapiUtils.getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getActiveTopologyOptions()
);
},
export function clickNode(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_NODE,
nodeId: nodeId
});
updateRoute();
getNodeDetails(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId()
);
}
openWebsocket: function() {
AppDispatcher.dispatch({
type: ActionTypes.OPEN_WEBSOCKET
});
},
export function clickTopology(topologyId) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_TOPOLOGY,
topologyId: topologyId
});
updateRoute();
getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getActiveTopologyOptions()
);
}
clearControlError: function() {
AppDispatcher.dispatch({
type: ActionTypes.CLEAR_CONTROL_ERROR
});
},
export function openWebsocket() {
AppDispatcher.dispatch({
type: ActionTypes.OPEN_WEBSOCKET
});
}
closeWebsocket: function() {
AppDispatcher.dispatch({
type: ActionTypes.CLOSE_WEBSOCKET
});
},
export function clearControlError() {
AppDispatcher.dispatch({
type: ActionTypes.CLEAR_CONTROL_ERROR
});
}
doControl: function(probeId, nodeId, control) {
AppDispatcher.dispatch({
type: ActionTypes.DO_CONTROL
});
WebapiUtils.doControl(
probeId,
nodeId,
control
);
},
export function closeWebsocket() {
AppDispatcher.dispatch({
type: ActionTypes.CLOSE_WEBSOCKET
});
}
enterEdge: function(edgeId) {
AppDispatcher.dispatch({
type: ActionTypes.ENTER_EDGE,
edgeId: edgeId
});
},
export function doControl(probeId, nodeId, control) {
AppDispatcher.dispatch({
type: ActionTypes.DO_CONTROL
});
doControlRequest(
probeId,
nodeId,
control
);
}
enterNode: function(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.ENTER_NODE,
nodeId: nodeId
});
},
export function enterEdge(edgeId) {
AppDispatcher.dispatch({
type: ActionTypes.ENTER_EDGE,
edgeId: edgeId
});
}
hitEsc: function() {
AppDispatcher.dispatch({
type: ActionTypes.HIT_ESC_KEY
});
RouterUtils.updateRoute();
},
export function enterNode(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.ENTER_NODE,
nodeId: nodeId
});
}
leaveEdge: function(edgeId) {
AppDispatcher.dispatch({
type: ActionTypes.LEAVE_EDGE,
edgeId: edgeId
});
},
export function hitEsc() {
AppDispatcher.dispatch({
type: ActionTypes.HIT_ESC_KEY
});
updateRoute();
}
leaveNode: function(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.LEAVE_NODE,
nodeId: nodeId
});
},
export function leaveEdge(edgeId) {
AppDispatcher.dispatch({
type: ActionTypes.LEAVE_EDGE,
edgeId: edgeId
});
}
receiveControlError: function(err) {
AppDispatcher.dispatch({
type: ActionTypes.DO_CONTROL_ERROR,
error: err
});
},
export function leaveNode(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.LEAVE_NODE,
nodeId: nodeId
});
}
receiveControlSuccess: function() {
AppDispatcher.dispatch({
type: ActionTypes.DO_CONTROL_SUCCESS
});
},
export function receiveControlError(err) {
AppDispatcher.dispatch({
type: ActionTypes.DO_CONTROL_ERROR,
error: err
});
}
receiveNodeDetails: function(details) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODE_DETAILS,
details: details
});
},
export function receiveControlSuccess() {
AppDispatcher.dispatch({
type: ActionTypes.DO_CONTROL_SUCCESS
});
}
receiveNodesDelta: function(delta) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODES_DELTA,
delta: delta
});
},
export function receiveNodeDetails(details) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODE_DETAILS,
details: details
});
}
receiveTopologies: function(topologies) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_TOPOLOGIES,
topologies: topologies
});
WebapiUtils.getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getActiveTopologyOptions()
);
WebapiUtils.getNodeDetails(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId()
);
},
export function receiveNodesDelta(delta) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODES_DELTA,
delta: delta
});
}
receiveApiDetails: function(apiDetails) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_API_DETAILS,
version: apiDetails.version
});
},
export function receiveTopologies(topologies) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_TOPOLOGIES,
topologies: topologies
});
getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getActiveTopologyOptions()
);
getNodeDetails(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId()
);
}
receiveError: function(errorUrl) {
AppDispatcher.dispatch({
errorUrl: errorUrl,
type: ActionTypes.RECEIVE_ERROR
});
},
export function receiveApiDetails(apiDetails) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_API_DETAILS,
version: apiDetails.version
});
}
route: function(state) {
AppDispatcher.dispatch({
state: state,
type: ActionTypes.ROUTE_TOPOLOGY
});
WebapiUtils.getTopologies(
AppStore.getActiveTopologyOptions()
);
WebapiUtils.getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getActiveTopologyOptions()
);
WebapiUtils.getNodeDetails(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId()
);
}
};
export function receiveError(errorUrl) {
AppDispatcher.dispatch({
errorUrl: errorUrl,
type: ActionTypes.RECEIVE_ERROR
});
}
// require below export to break circular dep
AppDispatcher = require('../dispatcher/app-dispatcher');
ActionTypes = require('../constants/action-types');
RouterUtils = require('../utils/router-utils');
WebapiUtils = require('../utils/web-api-utils');
AppStore = require('../stores/app-store');
export function route(state) {
AppDispatcher.dispatch({
state: state,
type: ActionTypes.ROUTE_TOPOLOGY
});
getTopologies(
AppStore.getActiveTopologyOptions()
);
getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getActiveTopologyOptions()
);
getNodeDetails(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId()
);
}

View File

@@ -1,17 +1,16 @@
const _ = require('lodash');
const d3 = require('d3');
const React = require('react');
const Motion = require('react-motion').Motion;
const spring = require('react-motion').spring;
import _ from 'lodash';
import d3 from 'd3';
import React from 'react';
import { Motion, spring } from 'react-motion';
const AppActions = require('../actions/app-actions');
import { enterEdge, leaveEdge } from '../actions/app-actions';
const line = d3.svg.line()
.interpolate('basis')
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
const animConfig = [80, 20]; // stiffness, bounce
const animConfig = [80, 20];// stiffness, bounce
const flattenPoints = function(points) {
const flattened = {};
@@ -35,23 +34,26 @@ const extractPoints = function(points) {
return extracted;
};
const Edge = React.createClass({
export default class Edge extends React.Component {
constructor(props, context) {
super(props, context);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
getInitialState: function() {
return {
this.state = {
points: []
};
},
}
componentWillMount: function() {
componentWillMount() {
this.ensureSameLength(this.props.points);
},
}
componentWillReceiveProps: function(nextProps) {
componentWillReceiveProps(nextProps) {
this.ensureSameLength(nextProps.points);
},
}
render: function() {
render() {
const classNames = ['edge'];
const points = flattenPoints(this.props.points);
const props = this.props;
@@ -79,9 +81,9 @@ const Edge = React.createClass({
}}
</Motion>
);
},
}
ensureSameLength: function(points) {
ensureSameLength(points) {
// Spring needs constant list length, hoping that dagre will insert never more than 10
const length = 10;
let missing = length - points.length;
@@ -92,16 +94,13 @@ const Edge = React.createClass({
}
return points;
},
handleMouseEnter: function(ev) {
AppActions.enterEdge(ev.currentTarget.id);
},
handleMouseLeave: function(ev) {
AppActions.leaveEdge(ev.currentTarget.id);
}
});
handleMouseEnter(ev) {
enterEdge(ev.currentTarget.id);
}
module.exports = Edge;
handleMouseLeave(ev) {
leaveEdge(ev.currentTarget.id);
}
}

View File

@@ -1,16 +1,18 @@
const React = require('react');
const Motion = require('react-motion').Motion;
const spring = require('react-motion').spring;
import React from 'react';
import { Motion, spring } from 'react-motion';
const AppActions = require('../actions/app-actions');
const NodeColorMixin = require('../mixins/node-color-mixin');
import { clickNode, enterNode, leaveNode } from '../actions/app-actions';
import { getNodeColor } from '../utils/color-utils';
const Node = React.createClass({
mixins: [
NodeColorMixin
],
export default class Node extends React.Component {
constructor(props, context) {
super(props, context);
this.handleMouseClick = this.handleMouseClick.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
}
render: function() {
render() {
const props = this.props;
const nodeScale = props.focused ? props.selectedNodeScale : props.nodeScale;
const zoomScale = this.props.zoomScale;
@@ -23,7 +25,7 @@ const Node = React.createClass({
let labelOffsetY = 18;
let subLabelOffsetY = 35;
const isPseudo = !!this.props.pseudo;
const color = isPseudo ? '' : this.getNodeColor(this.props.rank, this.props.label);
const color = isPseudo ? '' : getNodeColor(this.props.rank, this.props.label);
const onMouseEnter = this.handleMouseEnter;
const onMouseLeave = this.handleMouseLeave;
const onMouseClick = this.handleMouseClick;
@@ -83,9 +85,9 @@ const Node = React.createClass({
}}
</Motion>
);
},
}
ellipsis: function(text, fontSize, maxWidth) {
ellipsis(text, fontSize, maxWidth) {
const averageCharLength = fontSize / 1.5;
const allowedChars = maxWidth / averageCharLength;
let truncatedText = text;
@@ -93,21 +95,18 @@ const Node = React.createClass({
truncatedText = text.slice(0, allowedChars) + '...';
}
return truncatedText;
},
handleMouseClick: function(ev) {
ev.stopPropagation();
AppActions.clickNode(ev.currentTarget.id);
},
handleMouseEnter: function(ev) {
AppActions.enterNode(ev.currentTarget.id);
},
handleMouseLeave: function(ev) {
AppActions.leaveNode(ev.currentTarget.id);
}
});
handleMouseClick(ev) {
ev.stopPropagation();
clickNode(ev.currentTarget.id);
}
module.exports = Node;
handleMouseEnter(ev) {
enterNode(ev.currentTarget.id);
}
handleMouseLeave(ev) {
leaveNode(ev.currentTarget.id);
}
}

View File

@@ -1,17 +1,19 @@
const _ = require('lodash');
const d3 = require('d3');
const debug = require('debug')('scope:nodes-chart');
const React = require('react');
const makeMap = require('immutable').Map;
const timely = require('timely');
import _ from 'lodash';
import d3 from 'd3';
import debug from 'debug';
import React from 'react';
import { Map as makeMap } from 'immutable';
import timely from 'timely';
const AppActions = require('../actions/app-actions');
const AppStore = require('../stores/app-store');
const Edge = require('./edge');
const Naming = require('../constants/naming');
const NodesLayout = require('./nodes-layout');
const Node = require('./node');
const NodesError = require('./nodes-error');
import { clickCloseDetails } from '../actions/app-actions';
import AppStore from '../stores/app-store';
import Edge from './edge';
import { EDGE_ID_SEPARATOR } from '../constants/naming';
import { doLayout } from './nodes-layout';
import Node from './node';
import NodesError from './nodes-error';
const log = debug('scope:nodes-chart');
const MARGINS = {
top: 130,
@@ -24,10 +26,13 @@ const MARGINS = {
const radiusDensity = d3.scale.threshold()
.domain([3, 6]).range([2.5, 3.5, 3]);
const NodesChart = React.createClass({
export default class NodesChart extends React.Component {
constructor(props, context) {
super(props, context);
this.handleMouseClick = this.handleMouseClick.bind(this);
this.zoomed = this.zoomed.bind(this);
getInitialState: function() {
return {
this.state = {
nodes: makeMap(),
edges: makeMap(),
panTranslate: [0, 0],
@@ -37,23 +42,26 @@ const NodesChart = React.createClass({
hasZoomed: false,
maxNodesExceeded: false
};
},
}
componentWillMount: function() {
componentWillMount() {
const state = this.updateGraphState(this.props, this.state);
this.setState(state);
},
}
componentDidMount() {
// distinguish pan/zoom from click
this.isZooming = false;
componentDidMount: function() {
this.zoom = d3.behavior.zoom()
.scaleExtent([0.1, 2])
.on('zoom', this.zoomed);
d3.select('.nodes-chart svg')
.call(this.zoom);
},
}
componentWillReceiveProps: function(nextProps) {
componentWillReceiveProps(nextProps) {
// gather state, setState should be called only once here
const state = _.assign({}, this.state);
@@ -76,9 +84,9 @@ const NodesChart = React.createClass({
}
this.setState(state);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
// undoing .call(zoom)
d3.select('.nodes-chart svg')
@@ -87,9 +95,9 @@ const NodesChart = React.createClass({
.on('onmousewheel', null)
.on('dblclick.zoom', null)
.on('touchstart.zoom', null);
},
}
renderGraphNodes: function(nodes, nodeScale) {
renderGraphNodes(nodes, nodeScale) {
const hasSelectedNode = this.props.selectedNodeId && this.props.nodes.has(this.props.selectedNodeId);
const adjacency = hasSelectedNode ? AppStore.getAdjacentNodes(this.props.selectedNodeId) : null;
const onNodeClick = this.props.onNodeClick;
@@ -148,9 +156,9 @@ const NodesChart = React.createClass({
/>
);
});
},
}
renderGraphEdges: function(edges) {
renderGraphEdges(edges) {
const selectedNodeId = this.props.selectedNodeId;
const hasSelectedNode = selectedNodeId && this.props.nodes.has(selectedNodeId);
@@ -173,18 +181,18 @@ const NodesChart = React.createClass({
blurred={edge.get('blurred')} highlighted={edge.get('highlighted')} />
);
});
},
}
renderMaxNodesError: function(show) {
renderMaxNodesError(show) {
const errorHint = 'We\u0027re working on it, but for now, try a different view?';
return (
<NodesError faIconClass="fa-ban" hidden={!show}>
<div className="centered">Too many nodes to show in the browser.<br />{errorHint}</div>
</NodesError>
);
},
}
renderEmptyTopologyError: function(show) {
renderEmptyTopologyError(show) {
return (
<NodesError faIconClass="fa-circle-thin" hidden={!show}>
<div className="heading">Nothing to show. This can have any of these reasons:</div>
@@ -195,9 +203,9 @@ const NodesChart = React.createClass({
</ul>
</NodesError>
);
},
}
render: function() {
render() {
const nodeElements = this.renderGraphNodes(this.state.nodes, this.state.nodeScale);
const edgeElements = this.renderGraphEdges(this.state.edges, this.state.nodeScale);
const scale = this.state.scale;
@@ -224,9 +232,9 @@ const NodesChart = React.createClass({
</svg>
</div>
);
},
}
initNodes: function(topology) {
initNodes(topology) {
return topology.map((node, id) => {
// copy relevant fields to state nodes
return makeMap({
@@ -239,9 +247,9 @@ const NodesChart = React.createClass({
y: 0
});
});
},
}
initEdges: function(topology, stateNodes) {
initEdges(topology, stateNodes) {
let edges = makeMap();
topology.forEach(function(node, nodeId) {
@@ -249,14 +257,14 @@ const NodesChart = React.createClass({
if (adjacency) {
adjacency.forEach(function(adjacent) {
const edge = [nodeId, adjacent];
const edgeId = edge.join(Naming.EDGE_ID_SEPARATOR);
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)) {
debug('Missing edge node', edge[0], edge[1]);
log('Missing edge node', edge[0], edge[1]);
}
edges = edges.set(edgeId, makeMap({
@@ -271,9 +279,9 @@ const NodesChart = React.createClass({
});
return edges;
},
}
centerSelectedNode: function(props, state) {
centerSelectedNode(props, state) {
let stateNodes = state.nodes;
let stateEdges = state.edges;
const selectedLayoutNode = stateNodes.get(props.selectedNodeId);
@@ -345,19 +353,17 @@ const NodesChart = React.createClass({
edges: stateEdges,
nodes: stateNodes
};
},
}
isZooming: false, // distinguish pan/zoom from click
handleMouseClick: function() {
handleMouseClick() {
if (!this.isZooming) {
AppActions.clickCloseDetails();
clickCloseDetails();
} else {
this.isZooming = false;
}
},
}
restoreLayout: function(state) {
restoreLayout(state) {
// undo any pan/zooming that might have happened
this.zoom.scale(state.scale);
this.zoom.translate(state.panTranslate);
@@ -377,9 +383,9 @@ const NodesChart = React.createClass({
});
return { edges, nodes};
},
}
updateGraphState: function(props, state) {
updateGraphState(props, state) {
const n = props.nodes.size;
if (n === 0) {
@@ -401,10 +407,10 @@ const NodesChart = React.createClass({
topologyId: this.props.topologyId
};
const timedLayouter = timely(NodesLayout.doLayout);
const timedLayouter = timely(doLayout);
const graph = timedLayouter(stateNodes, stateEdges, options);
debug('graph layout took ' + timedLayouter.time + 'ms');
log('graph layout took ' + timedLayouter.time + 'ms');
// layout was aborted
if (!graph) {
@@ -443,17 +449,17 @@ const NodesChart = React.createClass({
nodeScale: nodeScale,
maxNodesExceeded: false
};
},
}
getNodeScale: function(props) {
getNodeScale(props) {
const expanse = Math.min(props.height, props.width);
const nodeSize = expanse / 3; // single node should fill a third of the screen
const maxNodeSize = expanse / 10;
const normalizedNodeSize = Math.min(nodeSize / Math.sqrt(props.nodes.size), maxNodeSize);
return this.state.nodeScale.copy().range([0, normalizedNodeSize]);
},
}
zoomed: function() {
zoomed() {
// debug('zoomed', d3.event.scale, d3.event.translate);
this.isZooming = true;
// dont pan while node is selected
@@ -465,7 +471,4 @@ const NodesChart = React.createClass({
});
}
}
});
module.exports = NodesChart;
}

View File

@@ -1,8 +1,7 @@
const React = require('react');
import React from 'react';
const NodesError = React.createClass({
render: function() {
export default class NodesError extends React.Component {
render() {
let classNames = 'nodes-chart-error';
if (this.props.hidden) {
classNames += ' hide';
@@ -18,7 +17,4 @@ const NodesError = React.createClass({
</div>
);
}
});
module.exports = NodesError;
}

View File

@@ -1,10 +1,11 @@
const dagre = require('dagre');
const debug = require('debug')('scope:nodes-layout');
const makeMap = require('immutable').Map;
const ImmSet = require('immutable').Set;
import dagre from 'dagre';
import debug from 'debug';
import { Map as makeMap, Set as ImmSet } from 'immutable';
const Naming = require('../constants/naming');
const TopologyUtils = require('./topology-utils');
import { EDGE_ID_SEPARATOR } from '../constants/naming';
import { updateNodeDegrees } from './topology-utils';
const log = debug('scope:nodes-layout');
const MAX_NODES = 100;
const topologyCaches = {};
@@ -32,7 +33,7 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) {
let edges = imEdges;
if (nodes.size > MAX_NODES) {
debug('Too many nodes for graph layout engine. Limit: ' + MAX_NODES);
log('Too many nodes for graph layout engine. Limit: ' + MAX_NODES);
return null;
}
@@ -82,7 +83,7 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) {
// remove edges that are no longer there
graph.edges().forEach(edgeObj => {
const edge = [edgeObj.v, edgeObj.w];
const edgeId = edge.join(Naming.EDGE_ID_SEPARATOR);
const edgeId = edge.join(EDGE_ID_SEPARATOR);
if (!edges.has(edgeId)) {
graph.removeEdge(edgeObj.v, edgeObj.w);
}
@@ -148,14 +149,14 @@ function layoutSingleNodes(layout, opts) {
const nonSingleNodes = nodes.filter(node => node.get('degree') !== 0);
if (nonSingleNodes.size > 0) {
if (aspectRatio < 1) {
debug('laying out single nodes to the right', aspectRatio);
log('laying out single nodes to the right', aspectRatio);
offsetX = nonSingleNodes.maxBy(node => node.get('x')).get('x');
offsetY = nonSingleNodes.minBy(node => node.get('y')).get('y');
if (offsetX) {
offsetX += nodeWidth + nodesep;
}
} else {
debug('laying out single nodes below', aspectRatio);
log('laying out single nodes below', aspectRatio);
offsetX = nonSingleNodes.minBy(node => node.get('x')).get('x');
offsetY = nonSingleNodes.maxBy(node => node.get('y')).get('y');
if (offsetY) {
@@ -264,7 +265,7 @@ export function hasUnseenNodes(nodes, cache) {
const hasUnseen = nodes.size > cache.size
|| !ImmSet.fromKeys(nodes).isSubset(ImmSet.fromKeys(cache));
if (hasUnseen) {
debug('unseen nodes:', ...ImmSet.fromKeys(nodes).subtract(ImmSet.fromKeys(cache)).toJS());
log('unseen nodes:', ...ImmSet.fromKeys(nodes).subtract(ImmSet.fromKeys(cache)).toJS());
}
return hasUnseen;
}
@@ -350,13 +351,13 @@ export function doLayout(immNodes, immEdges, opts) {
++layoutRuns;
if (cachedLayout && nodeCache && edgeCache && !hasUnseenNodes(immNodes, nodeCache)) {
debug('skip layout, trivial adjustment', ++layoutRunsTrivial, layoutRuns);
log('skip layout, trivial adjustment', ++layoutRunsTrivial, layoutRuns);
layout = cloneLayout(cachedLayout, immNodes, immEdges);
// copy old properties, works also if nodes get re-added
layout = copyLayoutProperties(layout, nodeCache, edgeCache);
} else {
const graph = cache.graph;
const nodesWithDegrees = TopologyUtils.updateNodeDegrees(immNodes, immEdges);
const nodesWithDegrees = updateNodeDegrees(immNodes, immEdges);
layout = runLayoutEngine(graph, nodesWithDegrees, immEdges, opts);
layout = layoutSingleNodes(layout, opts);
layout = shiftLayoutToCenter(layout, opts);

View File

@@ -1,19 +1,22 @@
import React from 'react';
import Immutable from 'immutable';
import TestUtils from 'react/lib/ReactTestUtils';
jest.dontMock('../../dispatcher/app-dispatcher');
jest.dontMock('../node-details.js');
jest.dontMock('../../mixins/node-color-mixin');
jest.dontMock('../../utils/color-utils');
jest.dontMock('../../utils/title-utils');
// need ES5 require to keep automocking off
const NodeDetails = require('../node-details.js').default;
describe('NodeDetails', () => {
let NodeDetails;
let nodes;
let nodeId;
let details;
const React = require('react');
const Immutable = require('immutable');
const TestUtils = require('react/lib/ReactTestUtils');
const makeMap = Immutable.OrderedMap;
beforeEach(() => {
NodeDetails = require('../node-details.js');
nodes = makeMap();
nodeId = 'n1';
});

View File

@@ -1,16 +1,16 @@
const React = require('react');
import React from 'react';
const Logo = require('./logo');
const AppStore = require('../stores/app-store');
const Sidebar = require('./sidebar.js');
const Status = require('./status.js');
const Topologies = require('./topologies.js');
const TopologyOptions = require('./topology-options.js');
const WebapiUtils = require('../utils/web-api-utils');
const AppActions = require('../actions/app-actions');
const Details = require('./details');
const Nodes = require('./nodes');
const RouterUtils = require('../utils/router-utils');
import Logo from './logo';
import AppStore from '../stores/app-store';
import Sidebar from './sidebar.js';
import Status from './status.js';
import Topologies from './topologies.js';
import TopologyOptions from './topology-options.js';
import { getApiDetails, getTopologies } from '../utils/web-api-utils';
import { hitEsc } from '../actions/app-actions';
import Details from './details';
import Nodes from './nodes';
import { getRouter } from '../utils/router-utils';
const ESC_KEY_CODE = 27;
@@ -36,35 +36,37 @@ function getStateFromStores() {
}
const App = React.createClass({
export default class App extends React.Component {
constructor(props, context) {
super(props, context);
this.onChange = this.onChange.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.state = getStateFromStores();
}
getInitialState: function() {
return getStateFromStores();
},
componentDidMount: function() {
AppStore.on(AppStore.CHANGE_EVENT, this.onChange);
componentDidMount() {
AppStore.addListener(this.onChange);
window.addEventListener('keyup', this.onKeyPress);
RouterUtils.getRouter().start({hashbang: true});
getRouter().start({hashbang: true});
if (!AppStore.isRouteSet()) {
// dont request topologies when already done via router
WebapiUtils.getTopologies(AppStore.getActiveTopologyOptions());
getTopologies(AppStore.getActiveTopologyOptions());
}
WebapiUtils.getApiDetails();
},
getApiDetails();
}
onChange: function() {
onChange() {
this.setState(getStateFromStores());
},
}
onKeyPress: function(ev) {
onKeyPress(ev) {
if (ev.keyCode === ESC_KEY_CODE) {
AppActions.hitEsc();
hitEsc();
}
},
}
render: function() {
render() {
const showingDetails = this.state.selectedNodeId;
const versionString = this.state.version ? 'Version ' + this.state.version : '';
// width of details panel blocking a view
@@ -104,8 +106,5 @@ const App = React.createClass({
</div>
</div>
);
},
});
module.exports = App;
}
}

View File

@@ -1,16 +1,20 @@
const React = require('react');
import React from 'react';
const AppActions = require('../actions/app-actions');
const NodeDetails = require('./node-details');
import { clickCloseDetails } from '../actions/app-actions';
import NodeDetails from './node-details';
const Details = React.createClass({
export default class Details extends React.Component {
constructor(props, context) {
super(props, context);
this.handleClickClose = this.handleClickClose.bind(this);
}
handleClickClose: function(ev) {
handleClickClose(ev) {
ev.preventDefault();
AppActions.clickCloseDetails();
},
clickCloseDetails();
}
render: function() {
render() {
return (
<div id="details">
<div style={{height: '100%', paddingBottom: 8, borderRadius: 2,
@@ -26,7 +30,4 @@ const Details = React.createClass({
</div>
);
}
});
module.exports = Details;
}

View File

@@ -1,8 +1,7 @@
const React = require('react');
import React from 'react';
const Logo = React.createClass({
render: function() {
export default class Logo extends React.Component {
render() {
return (
<div className="logo">
<svg width="100%" height="100%" viewBox="0 0 1089 217">
@@ -59,7 +58,4 @@ const Logo = React.createClass({
</div>
);
}
});
module.exports = Logo;
}

View File

@@ -1,28 +1,23 @@
const _ = require('lodash');
const React = require('react');
import _ from 'lodash';
import React from 'react';
const NodeDetailsControls = require('./node-details/node-details-controls');
const NodeDetailsTable = require('./node-details/node-details-table');
const NodeColorMixin = require('../mixins/node-color-mixin');
const TitleUtils = require('../utils/title-utils');
import NodeDetailsControls from './node-details/node-details-controls';
import NodeDetailsTable from './node-details/node-details-table';
import { brightenColor, getNodeColorDark } from '../utils/color-utils';
import { resetDocumentTitle, setDocumentTitle } from '../utils/title-utils';
const NodeDetails = React.createClass({
mixins: [
NodeColorMixin
],
componentDidMount: function() {
export default class NodeDetails extends React.Component {
componentDidMount() {
this.updateTitle();
},
}
componentWillUnmount: function() {
TitleUtils.resetTitle();
},
componentWillUnmount() {
resetDocumentTitle();
}
renderLoading: function() {
renderLoading() {
const node = this.props.nodes.get(this.props.nodeId);
const nodeColor = this.getNodeColorDark(node.get('rank'), node.get('label_major'));
const nodeColor = getNodeColorDark(node.get('rank'), node.get('label_major'));
const styles = {
header: {
'backgroundColor': nodeColor
@@ -48,9 +43,9 @@ const NodeDetails = React.createClass({
</div>
</div>
);
},
}
renderNotAvailable: function() {
renderNotAvailable() {
return (
<div className="node-details">
<div className="node-details-header node-details-header-notavailable">
@@ -71,9 +66,9 @@ const NodeDetails = React.createClass({
</div>
</div>
);
},
}
render: function() {
render() {
const details = this.props.details;
const nodeExists = this.props.nodes && this.props.nodes.has(this.props.nodeId);
@@ -86,14 +81,14 @@ const NodeDetails = React.createClass({
}
return this.renderLoading();
},
}
renderDetails: function() {
renderDetails() {
const details = this.props.details;
const nodeColor = this.getNodeColorDark(details.rank, details.label_major);
const nodeColor = getNodeColorDark(details.rank, details.label_major);
const styles = {
controls: {
'backgroundColor': this.brightenColor(nodeColor)
'backgroundColor': brightenColor(nodeColor)
},
header: {
'backgroundColor': nodeColor
@@ -126,16 +121,13 @@ const NodeDetails = React.createClass({
</div>
</div>
);
},
componentDidUpdate: function() {
this.updateTitle();
},
updateTitle: function() {
TitleUtils.setTitle(this.props.details && this.props.details.label_major);
}
});
componentDidUpdate() {
this.updateTitle();
}
module.exports = NodeDetails;
updateTitle() {
setDocumentTitle(this.props.details && this.props.details.label_major);
}
}

View File

@@ -1,10 +1,14 @@
const React = require('react');
import React from 'react';
const AppActions = require('../../actions/app-actions');
import { doControl } from '../../actions/app-actions';
const NodeDetailsControlButton = React.createClass({
export default class NodeDetailsControlButton extends React.Component {
constructor(props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
}
render: function() {
render() {
let className = `node-control-button fa ${this.props.control.icon}`;
if (this.props.pending) {
className += ' node-control-button-pending';
@@ -12,13 +16,10 @@ const NodeDetailsControlButton = React.createClass({
return (
<span className={className} title={this.props.control.human} onClick={this.handleClick} />
);
},
handleClick: function(ev) {
ev.preventDefault();
AppActions.doControl(this.props.control.probeId, this.props.control.nodeId, this.props.control.id);
}
});
module.exports = NodeDetailsControlButton;
handleClick(ev) {
ev.preventDefault();
doControl(this.props.control.probeId, this.props.control.nodeId, this.props.control.id);
}
}

View File

@@ -1,10 +1,9 @@
const React = require('react');
import React from 'react';
const NodeDetailsControlButton = require('./node-details-control-button');
import NodeDetailsControlButton from './node-details-control-button';
const NodeDetailsControls = React.createClass({
render: function() {
export default class NodeDetailsControls extends React.Component {
render() {
let spinnerClassName = 'fa fa-circle-o-notch fa-spin';
if (this.props.pending) {
spinnerClassName += ' node-details-controls-spinner';
@@ -30,7 +29,4 @@ const NodeDetailsControls = React.createClass({
</div>
);
}
});
module.exports = NodeDetailsControls;
}

View File

@@ -1,8 +1,7 @@
const React = require('react');
import React from 'react';
const NodeDetailsTableRowNumber = React.createClass({
render: function() {
export default class NodeDetailsTableRowNumber extends React.Component {
render() {
const row = this.props.row;
return (
<div className="node-details-table-row-value">
@@ -11,6 +10,4 @@ const NodeDetailsTableRowNumber = React.createClass({
</div>
);
}
});
module.exports = NodeDetailsTableRowNumber;
}

View File

@@ -1,10 +1,9 @@
const React = require('react');
import React from 'react';
const Sparkline = require('../sparkline');
import Sparkline from '../sparkline';
const NodeDetailsTableRowSparkline = React.createClass({
render: function() {
export default class NodeDetailsTableRowSparkline extends React.Component {
render() {
const row = this.props.row;
return (
<div className="node-details-table-row-value">
@@ -13,6 +12,4 @@ const NodeDetailsTableRowSparkline = React.createClass({
</div>
);
}
});
module.exports = NodeDetailsTableRowSparkline;
}

View File

@@ -1,8 +1,7 @@
const React = require('react');
import React from 'react';
const NodeDetailsTableRowValue = React.createClass({
render: function() {
export default class NodeDetailsTableRowValue extends React.Component {
render() {
const row = this.props.row;
return (
<div className="node-details-table-row-value">
@@ -15,6 +14,4 @@ const NodeDetailsTableRowValue = React.createClass({
</div>
);
}
});
module.exports = NodeDetailsTableRowValue;
}

View File

@@ -1,12 +1,11 @@
const React = require('react');
import React from 'react';
const NodeDetailsTableRowValue = require('./node-details-table-row-value');
const NodeDetailsTableRowNumber = require('./node-details-table-row-number');
const NodeDetailsTableRowSparkline = require('./node-details-table-row-sparkline');
import NodeDetailsTableRowValue from './node-details-table-row-value';
import NodeDetailsTableRowNumber from './node-details-table-row-number';
import NodeDetailsTableRowSparkline from './node-details-table-row-sparkline';
const NodeDetailsTable = React.createClass({
render: function() {
export default class NodeDetailsTable extends React.Component {
render() {
return (
<div className="node-details-table">
<h4 className="node-details-table-title truncate" title={this.props.title}>
@@ -32,7 +31,4 @@ const NodeDetailsTable = React.createClass({
</div>
);
}
});
module.exports = NodeDetailsTable;
}

View File

@@ -1,28 +1,30 @@
const React = require('react');
import React from 'react';
const NodesChart = require('../charts/nodes-chart');
import NodesChart from '../charts/nodes-chart';
const navbarHeight = 160;
const marginTop = 0;
const Nodes = React.createClass({
export default class Nodes extends React.Component {
constructor(props, context) {
super(props, context);
this.handleResize = this.handleResize.bind(this);
getInitialState: function() {
return {
this.state = {
width: window.innerWidth,
height: window.innerHeight - navbarHeight - marginTop
};
},
}
componentDidMount: function() {
componentDidMount() {
window.addEventListener('resize', this.handleResize);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
},
}
render: function() {
render() {
return (
<NodesChart
highlightedEdgeIds={this.props.highlightedEdgeIds}
@@ -36,19 +38,16 @@ const Nodes = React.createClass({
topMargin={this.props.topMargin}
/>
);
},
}
handleResize: function() {
handleResize() {
this.setDimensions();
},
}
setDimensions: function() {
setDimensions() {
const width = window.innerWidth;
const height = window.innerHeight - navbarHeight - marginTop;
this.setState({height, width});
}
});
module.exports = Nodes;
}

View File

@@ -1,15 +1,11 @@
const React = require('react');
import React from 'react';
const Sidebar = React.createClass({
render: function() {
export default class Sidebar extends React.Component {
render() {
return (
<div className="sidebar">
{this.props.children}
</div>
);
}
});
module.exports = Sidebar;
}

View File

@@ -1,26 +1,14 @@
// Forked from: https://github.com/KyleAMathews/react-sparkline at commit a9d7c5203d8f240938b9f2288287aaf0478df013
const React = require('react');
const ReactDOM = require('react-dom');
const d3 = require('d3');
import React from 'react';
import ReactDOM from 'react-dom';
import d3 from 'd3';
const Sparkline = React.createClass({
getDefaultProps: function() {
return {
width: 100,
height: 16,
strokeColor: '#7d7da8',
strokeWidth: '0.5px',
interpolate: 'basis',
circleDiameter: 1.75,
data: [1, 23, 5, 5, 23, 0, 0, 0, 4, 32, 3, 12, 3, 1, 24, 1, 5, 5, 24, 23] // Some semi-random data.
};
},
componentDidMount: function() {
export default class Sparkline extends React.Component {
componentDidMount() {
return this.renderSparkline();
},
}
renderSparkline: function() {
renderSparkline() {
// If the sparkline has already been rendered, remove it.
const el = ReactDOM.findDOMNode(this);
while (el.firstChild) {
@@ -114,17 +102,25 @@ const Sparkline = React.createClass({
attr('fill-opacity', 0.6).
attr('stroke', 'none').
attr('r', this.props.circleDiameter);
},
}
render: function() {
render() {
return (
<div/>
);
},
}
componentDidUpdate: function() {
componentDidUpdate() {
return this.renderSparkline();
}
});
}
module.exports = Sparkline;
Sparkline.defaultProps = {
width: 100,
height: 16,
strokeColor: '#7d7da8',
strokeWidth: '0.5px',
interpolate: 'basis',
circleDiameter: 1.75,
data: [1, 23, 5, 5, 23, 0, 0, 0, 4, 32, 3, 12, 3, 1, 24, 1, 5, 5, 24, 23] // Some semi-random data.
};

View File

@@ -1,8 +1,7 @@
const React = require('react');
import React from 'react';
const Status = React.createClass({
render: function() {
export default class Status extends React.Component {
render() {
let title = '';
let text = 'Trying to reconnect...';
let showWarningIcon = false;
@@ -36,7 +35,4 @@ const Status = React.createClass({
</div>
);
}
});
module.exports = Status;
}

View File

@@ -1,16 +1,21 @@
const React = require('react');
const _ = require('lodash');
import React from 'react';
import _ from 'lodash';
const AppActions = require('../actions/app-actions');
import { clickTopology } from '../actions/app-actions';
const Topologies = React.createClass({
export default class Topologies extends React.Component {
constructor(props, context) {
super(props, context);
this.onTopologyClick = this.onTopologyClick.bind(this);
this.renderSubTopology = this.renderSubTopology.bind(this);
}
onTopologyClick: function(ev) {
onTopologyClick(ev) {
ev.preventDefault();
AppActions.clickTopology(ev.currentTarget.getAttribute('rel'));
},
clickTopology(ev.currentTarget.getAttribute('rel'));
}
renderSubTopology: function(subTopology) {
renderSubTopology(subTopology) {
const isActive = subTopology.name === this.props.currentTopology.name;
const topologyId = subTopology.id;
const title = this.renderTitle(subTopology);
@@ -24,14 +29,14 @@ const Topologies = React.createClass({
</div>
</div>
);
},
}
renderTitle: function(topology) {
renderTitle(topology) {
return ['Nodes: ' + topology.stats.node_count,
'Connections: ' + topology.stats.node_count].join('\n');
},
}
renderTopology: function(topology) {
renderTopology(topology) {
const isActive = topology.name === this.props.currentTopology.name;
const className = isActive ? 'topologies-item-main topologies-item-main-active' : 'topologies-item-main';
const topologyId = topology.id;
@@ -49,9 +54,9 @@ const Topologies = React.createClass({
</div>
</div>
);
},
}
render: function() {
render() {
const topologies = _.sortBy(this.props.topologies, function(topology) {
return topology.name;
});
@@ -64,7 +69,4 @@ const Topologies = React.createClass({
</div>
);
}
});
module.exports = Topologies;
}

View File

@@ -1,22 +1,23 @@
const React = require('react');
import React from 'react';
const AppActions = require('../actions/app-actions');
import { changeTopologyOption } from '../actions/app-actions';
const TopologyOptionAction = React.createClass({
export default class TopologyOptionAction extends React.Component {
constructor(props, context) {
super(props, context);
this.onClick = this.onClick.bind(this);
}
onClick: function(ev) {
onClick(ev) {
ev.preventDefault();
AppActions.changeTopologyOption(this.props.option, this.props.value, this.props.topologyId);
},
changeTopologyOption(this.props.option, this.props.value, this.props.topologyId);
}
render: function() {
render() {
return (
<span className="sidebar-item-action" onClick={this.onClick}>
{this.props.value}
</span>
);
}
});
module.exports = TopologyOptionAction;
}

View File

@@ -1,22 +1,21 @@
const React = require('react');
const _ = require('lodash');
import React from 'react';
import _ from 'lodash';
const TopologyOptionAction = require('./topology-option-action');
import TopologyOptionAction from './topology-option-action';
const TopologyOptions = React.createClass({
renderAction: function(action, option, topologyId) {
export default class TopologyOptions extends React.Component {
renderAction(action, option, topologyId) {
return (
<TopologyOptionAction option={option} value={action} topologyId={topologyId} key={action} />
);
},
}
/**
* transforms a list of options into one sidebar-item.
* The sidebar text comes from the active option. the actions come from the
* remaining items.
*/
renderOption: function(items) {
renderOption(items) {
let activeText;
let activeValue;
const actions = [];
@@ -53,9 +52,9 @@ const TopologyOptions = React.createClass({
</span>
</div>
);
},
}
render: function() {
render() {
const options = _.sortBy(
_.map(this.props.options, function(items, optionId) {
_.each(items, function(item) {
@@ -75,7 +74,4 @@ const TopologyOptions = React.createClass({
</div>
);
}
});
module.exports = TopologyOptions;
}

View File

@@ -1,26 +1,28 @@
const keymirror = require('keymirror');
import _ from 'lodash';
module.exports = keymirror({
CHANGE_TOPOLOGY_OPTION: null,
CLEAR_CONTROL_ERROR: null,
CLICK_CLOSE_DETAILS: null,
CLICK_NODE: null,
CLICK_TOPOLOGY: null,
CLOSE_WEBSOCKET: null,
DO_CONTROL: null,
DO_CONTROL_ERROR: null,
DO_CONTROL_SUCCESS: null,
ENTER_EDGE: null,
ENTER_NODE: null,
HIT_ESC_KEY: null,
LEAVE_EDGE: null,
LEAVE_NODE: null,
OPEN_WEBSOCKET: null,
RECEIVE_NODE_DETAILS: null,
RECEIVE_NODES: null,
RECEIVE_NODES_DELTA: null,
RECEIVE_TOPOLOGIES: null,
RECEIVE_API_DETAILS: null,
RECEIVE_ERROR: null,
ROUTE_TOPOLOGY: null
});
const ACTION_TYPES = [
'CHANGE_TOPOLOGY_OPTION',
'CLEAR_CONTROL_ERROR',
'CLICK_CLOSE_DETAILS',
'CLICK_NODE',
'CLICK_TOPOLOGY',
'CLOSE_WEBSOCKET',
'DO_CONTROL',
'DO_CONTROL_ERROR',
'DO_CONTROL_SUCCESS',
'ENTER_EDGE',
'ENTER_NODE',
'HIT_ESC_KEY',
'LEAVE_EDGE',
'LEAVE_NODE',
'OPEN_WEBSOCKET',
'RECEIVE_NODE_DETAILS',
'RECEIVE_NODES',
'RECEIVE_NODES_DELTA',
'RECEIVE_TOPOLOGIES',
'RECEIVE_API_DETAILS',
'RECEIVE_ERROR',
'ROUTE_TOPOLOGY'
];
export default _.zipObject(ACTION_TYPES, ACTION_TYPES);

View File

@@ -1,4 +1,2 @@
module.exports = {
EDGE_ID_SEPARATOR: '-'
};
export const EDGE_ID_SEPARATOR = '-';

View File

@@ -1,12 +1,13 @@
const flux = require('flux');
const _ = require('lodash');
import { Dispatcher } from 'flux';
import _ from 'lodash';
const AppDispatcher = new flux.Dispatcher();
const instance = new Dispatcher();
AppDispatcher.dispatch = _.wrap(flux.Dispatcher.prototype.dispatch, function(func) {
instance.dispatch = _.wrap(Dispatcher.prototype.dispatch, function(func) {
const args = Array.prototype.slice.call(arguments, 1);
// console.log(args[0]);
func.apply(this, args);
});
module.exports = AppDispatcher;
export default instance;
export const dispatch = instance.dispatch.bind(instance);

View File

@@ -1,10 +1,10 @@
require('font-awesome-webpack');
require('../styles/main.less');
const React = require('react');
const ReactDOM = require('react-dom');
import React from 'react';
import ReactDOM from 'react-dom';
const App = require('./components/app.js');
import App from './components/app.js';
ReactDOM.render(
<App/>,

View File

@@ -4,7 +4,7 @@ jest.dontMock('../app-store');
// Appstore test suite using Jasmine matchers
describe('AppStore', function() {
const ActionTypes = require('../../constants/action-types');
const ActionTypes = require('../../constants/action-types').default;
let AppDispatcher;
let AppStore;
let registeredCallback;
@@ -134,9 +134,10 @@ describe('AppStore', function() {
};
beforeEach(function() {
AppDispatcher = require('../../dispatcher/app-dispatcher');
AppStore = require('../app-store');
registeredCallback = AppDispatcher.register.mock.calls[0][0];
AppStore = require('../app-store').default;
AppDispatcher = AppStore.getDispatcher();
const callback = AppDispatcher.dispatch.bind(AppDispatcher);
registeredCallback = callback;
});
// topology tests

View File

@@ -1,14 +1,15 @@
const EventEmitter = require('events').EventEmitter;
const _ = require('lodash');
const debug = require('debug')('scope:app-store');
const Immutable = require('immutable');
import _ from 'lodash';
import debug from 'debug';
import Immutable from 'immutable';
import { Store } from 'flux/utils';
const AppDispatcher = require('../dispatcher/app-dispatcher');
const ActionTypes = require('../constants/action-types');
const Naming = require('../constants/naming');
import AppDispatcher from '../dispatcher/app-dispatcher';
import ActionTypes from '../constants/action-types';
import { EDGE_ID_SEPARATOR } from '../constants/naming';
const makeOrderedMap = Immutable.OrderedMap;
const makeSet = Immutable.Set;
const log = debug('scope:app-store');
// Helpers
@@ -104,25 +105,23 @@ function deSelectNode() {
// Store API
const AppStore = Object.assign({}, EventEmitter.prototype, {
CHANGE_EVENT: 'change',
export class AppStore extends Store {
// keep at the top
getAppState: function() {
getAppState() {
return {
topologyId: currentTopologyId,
selectedNodeId: this.getSelectedNodeId(),
topologyOptions: topologyOptions.toJS() // all options
};
},
}
getActiveTopologyOptions: function() {
getActiveTopologyOptions() {
// options for current topology
return topologyOptions.get(currentTopologyId);
},
}
getAdjacentNodes: function(nodeId) {
getAdjacentNodes(nodeId) {
adjacentNodes = adjacentNodes.clear();
if (nodes.has(nodeId)) {
@@ -136,36 +135,36 @@ const AppStore = Object.assign({}, EventEmitter.prototype, {
}
return adjacentNodes;
},
}
getControlError: function() {
getControlError() {
return controlError;
},
}
getCurrentTopology: function() {
getCurrentTopology() {
if (!currentTopology) {
currentTopology = setTopology(currentTopologyId);
}
return currentTopology;
},
}
getCurrentTopologyId: function() {
getCurrentTopologyId() {
return currentTopologyId;
},
}
getCurrentTopologyOptions: function() {
getCurrentTopologyOptions() {
return currentTopology && currentTopology.options;
},
}
getCurrentTopologyUrl: function() {
getCurrentTopologyUrl() {
return currentTopology && currentTopology.url;
},
}
getErrorUrl: function() {
getErrorUrl() {
return errorUrl;
},
}
getHighlightedEdgeIds: function() {
getHighlightedEdgeIds() {
if (mouseOverNodeId && nodes.has(mouseOverNodeId)) {
// all neighbour combinations because we dont know which direction exists
const adjacency = nodes.get(mouseOverNodeId).get('adjacency');
@@ -173,8 +172,8 @@ const AppStore = Object.assign({}, EventEmitter.prototype, {
return _.flatten(
adjacency.forEach(function(nodeId) {
return [
[nodeId, mouseOverNodeId].join(Naming.EDGE_ID_SEPARATOR),
[mouseOverNodeId, nodeId].join(Naming.EDGE_ID_SEPARATOR)
[nodeId, mouseOverNodeId].join(EDGE_ID_SEPARATOR),
[mouseOverNodeId, nodeId].join(EDGE_ID_SEPARATOR)
];
})
);
@@ -184,9 +183,9 @@ const AppStore = Object.assign({}, EventEmitter.prototype, {
return mouseOverEdgeId;
}
return null;
},
}
getHighlightedNodeIds: function() {
getHighlightedNodeIds() {
if (mouseOverNodeId) {
const adjacency = this.getAdjacentNodes(mouseOverNodeId);
if (adjacency.size) {
@@ -194,243 +193,238 @@ const AppStore = Object.assign({}, EventEmitter.prototype, {
}
}
if (mouseOverEdgeId) {
return mouseOverEdgeId.split(Naming.EDGE_ID_SEPARATOR);
return mouseOverEdgeId.split(EDGE_ID_SEPARATOR);
}
return null;
},
}
getNodeDetails: function() {
getNodeDetails() {
return nodeDetails;
},
}
getNodes: function() {
getNodes() {
return nodes;
},
}
getSelectedNodeId: function() {
getSelectedNodeId() {
return selectedNodeId;
},
}
getTopologies: function() {
getTopologies() {
return topologies;
},
}
getVersion: function() {
getVersion() {
return version;
},
}
isControlPending: function() {
isControlPending() {
return controlPending;
},
}
isRouteSet: function() {
isRouteSet() {
return routeSet;
},
}
isTopologiesLoaded: function() {
isTopologiesLoaded() {
return topologiesLoaded;
},
}
isTopologyEmpty: function() {
isTopologyEmpty() {
return currentTopology && currentTopology.stats && currentTopology.stats.node_count === 0 && nodes.size === 0;
},
}
isWebsocketClosed: function() {
isWebsocketClosed() {
return websocketClosed;
}
});
__onDispatch(payload) {
switch (payload.type) {
// Store Dispatch Hooks
AppStore.registeredCallback = function(payload) {
switch (payload.type) {
case ActionTypes.CHANGE_TOPOLOGY_OPTION:
if (topologyOptions.getIn([payload.topologyId, payload.option])
!== payload.value) {
nodes = nodes.clear();
}
topologyOptions = topologyOptions.setIn(
[payload.topologyId, payload.option],
payload.value
);
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.CLEAR_CONTROL_ERROR:
controlError = null;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.CLICK_CLOSE_DETAILS:
deSelectNode();
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.CLICK_NODE:
deSelectNode();
if (payload.nodeId !== selectedNodeId) {
// select new node if it's not the same (in that case just delesect)
selectedNodeId = payload.nodeId;
}
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.CLICK_TOPOLOGY:
deSelectNode();
if (payload.topologyId !== currentTopologyId) {
setTopology(payload.topologyId);
nodes = nodes.clear();
}
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.CLOSE_WEBSOCKET:
websocketClosed = true;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.DO_CONTROL:
controlPending = true;
controlError = null;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.ENTER_EDGE:
mouseOverEdgeId = payload.edgeId;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.ENTER_NODE:
mouseOverNodeId = payload.nodeId;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.HIT_ESC_KEY:
deSelectNode();
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.LEAVE_EDGE:
mouseOverEdgeId = null;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.LEAVE_NODE:
mouseOverNodeId = null;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.OPEN_WEBSOCKET:
// flush nodes cache after re-connect
nodes = nodes.clear();
websocketClosed = false;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.DO_CONTROL_ERROR:
controlPending = false;
controlError = payload.error;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.DO_CONTROL_SUCCESS:
controlPending = false;
controlError = null;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.RECEIVE_ERROR:
errorUrl = payload.errorUrl;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.RECEIVE_NODE_DETAILS:
errorUrl = null;
// disregard if node is not selected anymore
if (payload.details.id === selectedNodeId) {
nodeDetails = payload.details;
}
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.RECEIVE_NODES_DELTA:
const emptyMessage = !payload.delta.add && !payload.delta.remove
&& !payload.delta.update;
if (!emptyMessage) {
debug('RECEIVE_NODES_DELTA',
'remove', _.size(payload.delta.remove),
'update', _.size(payload.delta.update),
'add', _.size(payload.delta.add));
}
errorUrl = null;
// nodes that no longer exist
_.each(payload.delta.remove, function(nodeId) {
// in case node disappears before mouseleave event
if (mouseOverNodeId === nodeId) {
mouseOverNodeId = null;
case ActionTypes.CHANGE_TOPOLOGY_OPTION:
if (topologyOptions.getIn([payload.topologyId, payload.option])
!== payload.value) {
nodes = nodes.clear();
}
if (nodes.has(nodeId) && _.contains(mouseOverEdgeId, nodeId)) {
mouseOverEdgeId = null;
topologyOptions = topologyOptions.setIn(
[payload.topologyId, payload.option],
payload.value
);
this.__emitChange();
break;
case ActionTypes.CLEAR_CONTROL_ERROR:
controlError = null;
this.__emitChange();
break;
case ActionTypes.CLICK_CLOSE_DETAILS:
deSelectNode();
this.__emitChange();
break;
case ActionTypes.CLICK_NODE:
deSelectNode();
if (payload.nodeId !== selectedNodeId) {
// select new node if it's not the same (in that case just delesect)
selectedNodeId = payload.nodeId;
}
nodes = nodes.delete(nodeId);
});
this.__emitChange();
break;
// update existing nodes
_.each(payload.delta.update, function(node) {
nodes = nodes.set(node.id, nodes.get(node.id).merge(makeNode(node)));
});
case ActionTypes.CLICK_TOPOLOGY:
deSelectNode();
if (payload.topologyId !== currentTopologyId) {
setTopology(payload.topologyId);
nodes = nodes.clear();
}
this.__emitChange();
break;
// add new nodes
_.each(payload.delta.add, function(node) {
nodes = nodes.set(node.id, Immutable.fromJS(makeNode(node)));
});
case ActionTypes.CLOSE_WEBSOCKET:
websocketClosed = true;
this.__emitChange();
break;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.DO_CONTROL:
controlPending = true;
controlError = null;
this.__emitChange();
break;
case ActionTypes.RECEIVE_TOPOLOGIES:
errorUrl = null;
topologies = processTopologies(payload.topologies);
setTopology(currentTopologyId);
// only set on first load, if options are not already set via route
if (!topologiesLoaded && topologyOptions.size === 0) {
case ActionTypes.ENTER_EDGE:
mouseOverEdgeId = payload.edgeId;
this.__emitChange();
break;
case ActionTypes.ENTER_NODE:
mouseOverNodeId = payload.nodeId;
this.__emitChange();
break;
case ActionTypes.HIT_ESC_KEY:
deSelectNode();
this.__emitChange();
break;
case ActionTypes.LEAVE_EDGE:
mouseOverEdgeId = null;
this.__emitChange();
break;
case ActionTypes.LEAVE_NODE:
mouseOverNodeId = null;
this.__emitChange();
break;
case ActionTypes.OPEN_WEBSOCKET:
// flush nodes cache after re-connect
nodes = nodes.clear();
websocketClosed = false;
this.__emitChange();
break;
case ActionTypes.DO_CONTROL_ERROR:
controlPending = false;
controlError = payload.error;
this.__emitChange();
break;
case ActionTypes.DO_CONTROL_SUCCESS:
controlPending = false;
controlError = null;
this.__emitChange();
break;
case ActionTypes.RECEIVE_ERROR:
errorUrl = payload.errorUrl;
this.__emitChange();
break;
case ActionTypes.RECEIVE_NODE_DETAILS:
errorUrl = null;
// disregard if node is not selected anymore
if (payload.details.id === selectedNodeId) {
nodeDetails = payload.details;
}
this.__emitChange();
break;
case ActionTypes.RECEIVE_NODES_DELTA:
const emptyMessage = !payload.delta.add && !payload.delta.remove
&& !payload.delta.update;
if (!emptyMessage) {
log('RECEIVE_NODES_DELTA',
'remove', _.size(payload.delta.remove),
'update', _.size(payload.delta.update),
'add', _.size(payload.delta.add));
}
errorUrl = null;
// nodes that no longer exist
_.each(payload.delta.remove, function(nodeId) {
// in case node disappears before mouseleave event
if (mouseOverNodeId === nodeId) {
mouseOverNodeId = null;
}
if (nodes.has(nodeId) && _.contains(mouseOverEdgeId, nodeId)) {
mouseOverEdgeId = null;
}
nodes = nodes.delete(nodeId);
});
// update existing nodes
_.each(payload.delta.update, function(node) {
nodes = nodes.set(node.id, nodes.get(node.id).merge(makeNode(node)));
});
// add new nodes
_.each(payload.delta.add, function(node) {
nodes = nodes.set(node.id, Immutable.fromJS(makeNode(node)));
});
this.__emitChange();
break;
case ActionTypes.RECEIVE_TOPOLOGIES:
errorUrl = null;
topologies = processTopologies(payload.topologies);
setTopology(currentTopologyId);
// only set on first load, if options are not already set via route
if (!topologiesLoaded && topologyOptions.size === 0) {
setDefaultTopologyOptions(topologies);
}
topologiesLoaded = true;
this.__emitChange();
break;
case ActionTypes.RECEIVE_API_DETAILS:
errorUrl = null;
version = payload.version;
this.__emitChange();
break;
case ActionTypes.ROUTE_TOPOLOGY:
routeSet = true;
if (currentTopologyId !== payload.state.topologyId) {
nodes = nodes.clear();
}
setTopology(payload.state.topologyId);
setDefaultTopologyOptions(topologies);
selectedNodeId = payload.state.selectedNodeId;
topologyOptions = Immutable.fromJS(payload.state.topologyOptions)
|| topologyOptions;
this.__emitChange();
break;
default:
break;
}
topologiesLoaded = true;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.RECEIVE_API_DETAILS:
errorUrl = null;
version = payload.version;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.ROUTE_TOPOLOGY:
routeSet = true;
if (currentTopologyId !== payload.state.topologyId) {
nodes = nodes.clear();
}
setTopology(payload.state.topologyId);
setDefaultTopologyOptions(topologies);
selectedNodeId = payload.state.selectedNodeId;
topologyOptions = Immutable.fromJS(payload.state.topologyOptions)
|| topologyOptions;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
default:
break;
}
};
}
AppStore.dispatchToken = AppDispatcher.register(AppStore.registeredCallback);
module.exports = AppStore;
export default new AppStore(AppDispatcher);

View File

@@ -1,4 +1,4 @@
const d3 = require('d3');
import d3 from 'd3';
const PSEUDO_COLOR = '#b1b1cb';
const hueRange = [20, 330]; // exclude red
@@ -40,35 +40,33 @@ function colors(text, secondText) {
return color;
}
const NodeColorMixin = {
getNodeColor: function(text, secondText) {
return colors(text, secondText);
},
getNodeColorDark: function(text, secondText) {
if (!text) {
return PSEUDO_COLOR;
}
const color = d3.rgb(colors(text, secondText));
let hsl = color.hsl();
export function getNodeColor(text, secondText) {
return colors(text, secondText);
}
// ensure darkness
if (hsl.l > 0.7) {
hsl = hsl.darker(1.5);
} else {
hsl = hsl.darker(1);
}
return hsl.toString();
},
brightenColor: function(color) {
let hsl = d3.rgb(color).hsl();
if (hsl.l > 0.5) {
hsl = hsl.brighter(0.5);
} else {
hsl = hsl.brighter(0.8);
}
return hsl.toString();
export function getNodeColorDark(text, secondText) {
if (!text) {
return PSEUDO_COLOR;
}
};
const color = d3.rgb(colors(text, secondText));
let hsl = color.hsl();
module.exports = NodeColorMixin;
// ensure darkness
if (hsl.l > 0.7) {
hsl = hsl.darker(1.5);
} else {
hsl = hsl.darker(1);
}
return hsl.toString();
}
export function brightenColor(color) {
let hsl = d3.rgb(color).hsl();
if (hsl.l > 0.5) {
hsl = hsl.brighter(0.5);
} else {
hsl = hsl.brighter(0.8);
}
return hsl.toString();
}

View File

@@ -1,9 +1,9 @@
const page = require('page');
import page from 'page';
const AppActions = require('../actions/app-actions');
const AppStore = require('../stores/app-store');
import { route } from '../actions/app-actions';
import AppStore from '../stores/app-store';
function updateRoute() {
export function updateRoute() {
const state = AppStore.getAppState();
const stateUrl = JSON.stringify(state);
const dispatch = false;
@@ -17,13 +17,9 @@ page('/', function() {
page('/state/:state', function(ctx) {
const state = JSON.parse(ctx.params.state);
AppActions.route(state);
route(state);
});
module.exports = {
getRouter: function() {
return page;
},
updateRoute: updateRoute
};
export function getRouter() {
return page;
}

View File

@@ -2,7 +2,7 @@
const PREFIX = 'Weave Scope';
const SEPARATOR = ' - ';
function setDocumentTitle(title) {
export function setDocumentTitle(title) {
if (title) {
document.title = [PREFIX, title].join(SEPARATOR);
} else {
@@ -10,11 +10,6 @@ function setDocumentTitle(title) {
}
}
function resetDocumentTitle() {
export function resetDocumentTitle() {
setDocumentTitle(null);
}
module.exports = {
resetTitle: resetDocumentTitle,
setTitle: setDocumentTitle
};

View File

@@ -1,11 +1,13 @@
import debug from 'debug';
import reqwest from 'reqwest';
const debug = require('debug')('scope:web-api-utils');
const reqwest = require('reqwest');
const AppActions = require('../actions/app-actions');
import { clearControlError, closeWebsocket, openWebsocket, receiveError,
receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError,
receiveControlSuccess, receiveTopologies } from '../actions/app-actions';
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = __WS_URL__ || wsProto + '://' + location.host + location.pathname.replace(/\/$/, '');
const log = debug('scope:web-api-utils');
const apiTimerInterval = 10000;
const reconnectTimerInterval = 5000;
@@ -40,14 +42,14 @@ function createWebsocket(topologyUrl, optionsQuery) {
+ '/ws?t=' + updateFrequency + '&' + optionsQuery);
socket.onopen = function() {
AppActions.openWebsocket();
openWebsocket();
};
socket.onclose = function() {
clearTimeout(reconnectTimer);
socket = null;
AppActions.closeWebsocket();
debug('Closed websocket to ' + topologyUrl);
closeWebsocket();
log('Closed websocket to ' + topologyUrl);
reconnectTimer = setTimeout(function() {
createWebsocket(topologyUrl, optionsQuery);
@@ -55,33 +57,33 @@ function createWebsocket(topologyUrl, optionsQuery) {
};
socket.onerror = function() {
debug('Error in websocket to ' + topologyUrl);
AppActions.receiveError(currentUrl);
log('Error in websocket to ' + topologyUrl);
receiveError(currentUrl);
};
socket.onmessage = function(event) {
const msg = JSON.parse(event.data);
AppActions.receiveNodesDelta(msg);
receiveNodesDelta(msg);
};
}
/* keep URLs relative */
function getTopologies(options) {
export function getTopologies(options) {
clearTimeout(topologyTimer);
const optionsQuery = buildOptionsQuery(options);
const url = `api/topology?${optionsQuery}`;
reqwest({
url: url,
success: function(res) {
AppActions.receiveTopologies(res);
receiveTopologies(res);
topologyTimer = setTimeout(function() {
getTopologies(options);
}, topologyTimerInterval / 2);
},
error: function(err) {
debug('Error in topology request: ' + err);
AppActions.receiveError(url);
log('Error in topology request: ' + err);
receiveError(url);
topologyTimer = setTimeout(function() {
getTopologies(options);
}, topologyTimerInterval / 2);
@@ -89,7 +91,7 @@ function getTopologies(options) {
});
}
function getTopology(topologyUrl, options) {
export function getNodesDelta(topologyUrl, options) {
const optionsQuery = buildOptionsQuery(options);
// only recreate websocket if url changed
@@ -100,44 +102,44 @@ function getTopology(topologyUrl, options) {
}
}
function getNodeDetails(topologyUrl, nodeId) {
export function getNodeDetails(topologyUrl, nodeId) {
if (topologyUrl && nodeId) {
const url = [topologyUrl, '/', encodeURIComponent(nodeId)]
.join('').substr(1);
reqwest({
url: url,
success: function(res) {
AppActions.receiveNodeDetails(res.node);
receiveNodeDetails(res.node);
},
error: function(err) {
debug('Error in node details request: ' + err.responseText);
log('Error in node details request: ' + err.responseText);
// dont treat missing node as error
if (err.status !== 404) {
AppActions.receiveError(topologyUrl);
receiveError(topologyUrl);
}
}
});
}
}
function getApiDetails() {
export function getApiDetails() {
clearTimeout(apiDetailsTimer);
const url = 'api';
reqwest({
url: url,
success: function(res) {
AppActions.receiveApiDetails(res);
receiveApiDetails(res);
apiDetailsTimer = setTimeout(getApiDetails, apiTimerInterval);
},
error: function(err) {
debug('Error in api details request: ' + err);
AppActions.receiveError(url);
log('Error in api details request: ' + err);
receiveError(url);
apiDetailsTimer = setTimeout(getApiDetails, apiTimerInterval / 2);
}
});
}
function doControl(probeId, nodeId, control) {
export function doControl(probeId, nodeId, control) {
clearTimeout(controlErrorTimer);
const url = `api/control/${encodeURIComponent(probeId)}/`
+ `${encodeURIComponent(nodeId)}/${control}`;
@@ -145,25 +147,13 @@ function doControl(probeId, nodeId, control) {
method: 'POST',
url: url,
success: function() {
AppActions.receiveControlSuccess();
receiveControlSuccess();
},
error: function(err) {
AppActions.receiveControlError(err.response);
receiveControlError(err.response);
controlErrorTimer = setTimeout(function() {
AppActions.clearControlError();
clearControlError();
}, 10000);
}
});
}
module.exports = {
doControl: doControl,
getNodeDetails: getNodeDetails,
getTopologies: getTopologies,
getApiDetails: getApiDetails,
getNodesDelta: getTopology
};

View File

@@ -13,7 +13,6 @@
"font-awesome": "4.4.0",
"font-awesome-webpack": "0.0.4",
"immutable": "~3.7.4",
"keymirror": "0.1.1",
"lodash": "~3.10.1",
"materialize-css": "0.97.2",
"page": "1.6.4",
@@ -83,6 +82,7 @@
"json"
],
"unmockedModulePathPatterns": [
"/dispatcher/",
"/node_modules/"
]
},

View File

@@ -22,7 +22,7 @@ var GLOBALS = {
module.exports = {
// Efficiently evaluate modules with source maps
devtool: 'eval',
devtool: 'cheap-module-source-map',
// Set entry point include necessary files for hot load
entry: [