Upgraded dev deps and linted JS according to latest airbnb rules

This commit is contained in:
David Kaltschmidt
2016-03-15 18:09:45 +01:00
parent 658a38f40a
commit e4270f69b7
49 changed files with 933 additions and 985 deletions

View File

@@ -7,7 +7,8 @@
},
"rules": {
"comma-dangle": 0,
"func-names": 0,
"object-curly-spacing": 0,
"react/jsx-closing-bracket-location": 0,
"react/sort-comp": 0,
"react/prop-types": 0
}

View File

@@ -15,9 +15,9 @@ const log = debug('scope:app-actions');
export function changeTopologyOption(option, value, topologyId) {
AppDispatcher.dispatch({
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
topologyId: topologyId,
option: option,
value: value
topologyId,
option,
value
});
updateRoute();
// update all request workers with new options
@@ -53,7 +53,7 @@ export function clickCloseDetails(nodeId) {
export function clickCloseTerminal(pipeId, closePipe) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_CLOSE_TERMINAL,
pipeId: pipeId
pipeId
});
if (closePipe) {
deletePipe(pipeId);
@@ -130,7 +130,7 @@ export function clickShowTopologyForNode(topologyId, nodeId) {
export function clickTopology(topologyId) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_TOPOLOGY,
topologyId: topologyId
topologyId
});
updateRoute();
resetUpdateBuffer();
@@ -149,7 +149,7 @@ export function openWebsocket() {
export function clearControlError(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.CLEAR_CONTROL_ERROR,
nodeId: nodeId
nodeId
});
}
@@ -162,7 +162,7 @@ export function closeWebsocket() {
export function doControl(nodeId, control) {
AppDispatcher.dispatch({
type: ActionTypes.DO_CONTROL,
nodeId: nodeId
nodeId
});
doControlRequest(nodeId, control);
}
@@ -170,14 +170,14 @@ export function doControl(nodeId, control) {
export function enterEdge(edgeId) {
AppDispatcher.dispatch({
type: ActionTypes.ENTER_EDGE,
edgeId: edgeId
edgeId
});
}
export function enterNode(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.ENTER_NODE,
nodeId: nodeId
nodeId
});
}
@@ -191,7 +191,7 @@ export function hitEsc() {
updateRoute();
// Dont deselect node on ESC if there is a controlPipe (keep terminal open)
} else if (AppStore.getTopCardNodeId() && !controlPipe) {
AppDispatcher.dispatch({type: ActionTypes.DESELECT_NODE});
AppDispatcher.dispatch({ type: ActionTypes.DESELECT_NODE });
updateRoute();
}
}
@@ -199,21 +199,21 @@ export function hitEsc() {
export function leaveEdge(edgeId) {
AppDispatcher.dispatch({
type: ActionTypes.LEAVE_EDGE,
edgeId: edgeId
edgeId
});
}
export function leaveNode(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.LEAVE_NODE,
nodeId: nodeId
nodeId
});
}
export function receiveControlError(nodeId, err) {
AppDispatcher.dispatch({
type: ActionTypes.DO_CONTROL_ERROR,
nodeId: nodeId,
nodeId,
error: err
});
}
@@ -221,14 +221,14 @@ export function receiveControlError(nodeId, err) {
export function receiveControlSuccess(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.DO_CONTROL_SUCCESS,
nodeId: nodeId
nodeId
});
}
export function receiveNodeDetails(details) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODE_DETAILS,
details: details
details
});
}
@@ -238,7 +238,7 @@ export function receiveNodesDelta(delta) {
} else {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODES_DELTA,
delta: delta
delta
});
}
}
@@ -246,7 +246,7 @@ export function receiveNodesDelta(delta) {
export function receiveTopologies(topologies) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_TOPOLOGIES,
topologies: topologies
topologies
});
getNodesDelta(
AppStore.getCurrentTopologyUrl(),
@@ -270,8 +270,8 @@ export function receiveControlPipeFromParams(pipeId, rawTty) {
// TODO add nodeId
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_CONTROL_PIPE,
pipeId: pipeId,
rawTty: rawTty
pipeId,
rawTty
});
}
@@ -289,9 +289,9 @@ export function receiveControlPipe(pipeId, nodeId, rawTty) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_CONTROL_PIPE,
nodeId: nodeId,
pipeId: pipeId,
rawTty: rawTty
nodeId,
pipeId,
rawTty
});
updateRoute();
@@ -300,14 +300,14 @@ export function receiveControlPipe(pipeId, nodeId, rawTty) {
export function receiveControlPipeStatus(pipeId, status) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_CONTROL_PIPE_STATUS,
pipeId: pipeId,
status: status
pipeId,
status
});
}
export function receiveError(errorUrl) {
AppDispatcher.dispatch({
errorUrl: errorUrl,
errorUrl,
type: ActionTypes.RECEIVE_ERROR
});
}
@@ -321,7 +321,7 @@ export function receiveNotFound(nodeId) {
export function route(state) {
AppDispatcher.dispatch({
state: state,
state,
type: ActionTypes.ROUTE_TOPOLOGY
});
getTopologies(

View File

@@ -7,23 +7,23 @@ 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; });
.x(d => d.x)
.y(d => d.y);
const animConfig = {stiffness: 80, damping: 20};
const flattenPoints = function(points) {
const flattenPoints = points => {
const flattened = {};
points.forEach(function(point, i) {
flattened['x' + i] = spring(point.x, animConfig);
flattened['y' + i] = spring(point.y, animConfig);
points.forEach((point, i) => {
flattened[`x${i}`] = spring(point.x, animConfig);
flattened[`y${i}`] = spring(point.y, animConfig);
});
return flattened;
};
const extractPoints = function(points) {
const extractPoints = points => {
const extracted = [];
_.each(points, function(value, key) {
_.each(points, (value, key) => {
const axis = key[0];
const index = key.slice(1);
if (!extracted[index]) {
@@ -70,10 +70,11 @@ export default class Edge extends React.Component {
return (
<Motion style={points}>
{function(interpolated) {
{(interpolated) => {
const path = line(extractPoints(interpolated));
return (
<g className={classes} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} id={props.id}>
<g className={classes} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} id={props.id}>
<path d={path} className="shadow" />
<path d={path} className="link" />
</g>

View File

@@ -3,7 +3,11 @@ import d3 from 'd3';
import { isContrastMode } from '../utils/contrast-utils';
const CLOUD_PATH = 'M 1920,384 Q 1920,225 1807.5,112.5 1695,0 1536,0 H 448 Q 263,0 131.5,131.5 0,263 0,448 0,580 71,689.5 142,799 258,853 q -2,28 -2,43 0,212 150,362 150,150 362,150 158,0 286.5,-88 128.5,-88 187.5,-230 70,62 166,62 106,0 181,-75 75,-75 75,-181 0,-75 -41,-138 129,-30 213,-134.5 84,-104.5 84,-239.5 z';
const CLOUD_PATH = 'M 1920,384 Q 1920,225 1807.5,112.5 1695,0 1536,0 H 448 '
+ 'Q 263,0 131.5,131.5 0,263 0,448 0,580 71,689.5 142,799 258,853 '
+ 'q -2,28 -2,43 0,212 150,362 150,150 362,150 158,0 286.5,-88 128.5,-88 '
+ '187.5,-230 70,62 166,62 106,0 181,-75 75,-75 75,-181 0,-75 -41,-138 '
+ '129,-30 213,-134.5 84,-104.5 84,-239.5 z';
function toPoint(stringPair) {
return stringPair.split(',').map(p => parseFloat(p, 10));
@@ -24,14 +28,12 @@ export default function NodeShapeCloud({highlighted, size, color}) {
const baseScale = (size * 2) / pathSize;
const strokeWidth = isContrastMode() ? 6 / baseScale : 4 / baseScale;
const pathProps = (v) => {
return {
d: CLOUD_PATH,
fill: 'none',
transform: `scale(-${v * baseScale}) translate(-${cx},-${cy})`,
strokeWidth
};
};
const pathProps = v => ({
d: CLOUD_PATH,
fill: 'none',
transform: `scale(-${v * baseScale}) translate(-${cx},-${cy})`,
strokeWidth
});
return (
<g className="shape shape-cloud">

View File

@@ -16,12 +16,10 @@ function polygon(r, sides) {
export default function NodeShapeHeptagon({onlyHighlight, highlighted, size, color}) {
const scaledSize = size * 1.0;
const pathProps = (v) => {
return {
d: line(polygon(scaledSize * v, 7)),
transform: `rotate(90)`
};
};
const pathProps = v => ({
d: line(polygon(scaledSize * v, 7)),
transform: 'rotate(90)'
});
const hightlightNode = <path className="highlighted" {...pathProps(0.7)} />;
@@ -42,4 +40,3 @@ export default function NodeShapeHeptagon({onlyHighlight, highlighted, size, col
</g>
);
}

View File

@@ -25,12 +25,10 @@ function getPoints(h) {
export default function NodeShapeHex({onlyHighlight, highlighted, size, color}) {
const pathProps = (v) => {
return {
d: getPoints(size * v * 2),
transform: `rotate(90) translate(-${size * getWidth(v)}, -${size * v})`
};
};
const pathProps = v => ({
d: getPoints(size * v * 2),
transform: `rotate(90) translate(-${size * getWidth(v)}, -${size * v})`
});
const hightlightNode = <path className="highlighted" {...pathProps(0.7)} />;

View File

@@ -1,15 +1,13 @@
import React from 'react';
export default function NodeShapeSquare({onlyHighlight, highlighted, size, color, rx = 0, ry = 0}) {
const rectProps = (v) => {
return {
width: v * size * 2,
height: v * size * 2,
rx: v * size * rx,
ry: v * size * ry,
transform: `translate(-${size * v}, -${size * v})`
};
};
const rectProps = v => ({
width: v * size * 2,
height: v * size * 2,
rx: v * size * rx,
ry: v * size * ry,
transform: `translate(-${size * v}, -${size * v})`
});
const hightlightNode = <rect className="highlighted" {...rectProps(0.7)} />;

View File

@@ -14,18 +14,15 @@ import NodeShapeCloud from './node-shape-cloud';
function stackedShape(Shape) {
const factory = React.createFactory(NodeShapeStack);
return function(props) {
return factory(Object.assign({}, props, {shape: Shape}));
};
return props => factory(Object.assign({}, props, {shape: Shape}));
}
const nodeShapes = {
'circle': NodeShapeCircle,
'hexagon': NodeShapeHex,
'heptagon': NodeShapeHeptagon,
'square': NodeShapeRoundedSquare,
'cloud': NodeShapeCloud
circle: NodeShapeCircle,
hexagon: NodeShapeHex,
heptagon: NodeShapeHeptagon,
square: NodeShapeRoundedSquare,
cloud: NodeShapeCloud
};
function getNodeShape({shape, stack}) {
@@ -98,7 +95,7 @@ export default class Node extends React.Component {
labelOffsetY: spring(labelOffsetY, animConfig),
subLabelOffsetY: spring(subLabelOffsetY, animConfig)
}}>
{function(interpolated) {
{(interpolated) => {
const transform = `translate(${interpolated.x},${interpolated.y})`;
return (
<g className={classes} transform={transform} id={props.id}
@@ -107,11 +104,13 @@ export default class Node extends React.Component {
size={nodeScale(interpolated.f)}
color={color}
{...props} />
<text className="node-label" textAnchor="middle" style={{fontSize: interpolated.labelFontSize}}
<text className="node-label" textAnchor="middle"
style={{fontSize: interpolated.labelFontSize}}
x="0" y={interpolated.labelOffsetY + nodeScale(0.5 * interpolated.f)}>
{label}
</text>
<text className="node-sublabel" textAnchor="middle" style={{fontSize: interpolated.subLabelFontSize}}
<text className="node-sublabel" textAnchor="middle"
style={{fontSize: interpolated.subLabelFontSize}}
x="0" y={interpolated.subLabelOffsetY + nodeScale(0.5 * interpolated.f)}>
{subLabel}
</text>
@@ -127,7 +126,7 @@ export default class Node extends React.Component {
const allowedChars = maxWidth / averageCharLength;
let truncatedText = text;
if (text && text.length > allowedChars) {
truncatedText = text.slice(0, allowedChars) + '...';
truncatedText = `${text.slice(0, allowedChars)}...`;
}
return truncatedText;
}

View File

@@ -100,7 +100,8 @@ export default class NodesChart extends React.Component {
}
renderGraphNodes(nodes, nodeScale) {
const hasSelectedNode = this.props.selectedNodeId && this.props.nodes.has(this.props.selectedNodeId);
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;
const zoomScale = this.state.scale;
@@ -117,9 +118,7 @@ export default class NodesChart extends React.Component {
&& (this.props.selectedNodeId === node.get('id') || adjacency.includes(node.get('id')));
return node.set('focused', focused);
};
const setBlurred = node => {
return node.set('blurred', hasSelectedNode && !node.get('focused'));
};
const setBlurred = node => node.set('blurred', hasSelectedNode && !node.get('focused'));
// make sure blurred nodes are in the background
const sortNodes = node => {
@@ -138,55 +137,49 @@ export default class NodesChart extends React.Component {
.map(setFocused)
.map(setBlurred)
.sortBy(sortNodes)
.map(node => {
return (<Node
blurred={node.get('blurred')}
focused={node.get('focused')}
highlighted={node.get('highlighted')}
topologyId={this.props.topologyId}
shape={node.get('shape')}
stack={node.get('stack')}
onClick={onNodeClick}
key={node.get('id')}
id={node.get('id')}
label={node.get('label')}
pseudo={node.get('pseudo')}
nodeCount={node.get('nodeCount')}
subLabel={node.get('subLabel')}
rank={node.get('rank')}
selectedNodeScale={selectedNodeScale}
nodeScale={nodeScale}
zoomScale={zoomScale}
dx={node.get('x')}
dy={node.get('y')}
/>
);
});
.map(node => <Node
blurred={node.get('blurred')}
focused={node.get('focused')}
highlighted={node.get('highlighted')}
topologyId={this.props.topologyId}
shape={node.get('shape')}
stack={node.get('stack')}
onClick={onNodeClick}
key={node.get('id')}
id={node.get('id')}
label={node.get('label')}
pseudo={node.get('pseudo')}
nodeCount={node.get('nodeCount')}
subLabel={node.get('subLabel')}
rank={node.get('rank')}
selectedNodeScale={selectedNodeScale}
nodeScale={nodeScale}
zoomScale={zoomScale}
dx={node.get('x')}
dy={node.get('y')}
/>);
}
renderGraphEdges(edges) {
const selectedNodeId = this.props.selectedNodeId;
const hasSelectedNode = selectedNodeId && this.props.nodes.has(selectedNodeId);
const setHighlighted = edge => {
return edge.set('highlighted', _.includes(this.props.highlightedEdgeIds, edge.get('id')));
};
const setBlurred = edge => {
return (edge.set('blurred', hasSelectedNode
&& edge.get('source') !== selectedNodeId
&& edge.get('target') !== selectedNodeId));
};
const setHighlighted = edge => edge.set('highlighted', _.includes(this.props.highlightedEdgeIds,
edge.get('id')));
const setBlurred = edge => edge.set('blurred', hasSelectedNode
&& edge.get('source') !== selectedNodeId
&& edge.get('target') !== selectedNodeId);
return edges
.toIndexedSeq()
.map(setHighlighted)
.map(setBlurred)
.map(edge => {
return (
<Edge key={edge.get('id')} id={edge.get('id')} points={edge.get('points')}
blurred={edge.get('blurred')} highlighted={edge.get('highlighted')} />
);
});
.map(edge => <Edge key={edge.get('id')} id={edge.get('id')}
points={edge.get('points')}
blurred={edge.get('blurred')} highlighted={edge.get('highlighted')}
/>
);
}
renderMaxNodesError(show) {
@@ -203,9 +196,12 @@ export default class NodesChart extends React.Component {
<NodesError faIconClass="fa-circle-thin" hidden={!show}>
<div className="heading">Nothing to show. This can have any of these reasons:</div>
<ul>
<li>We haven't received any reports from probes recently. Are the probes properly configured?</li>
<li>There are nodes, but they're currently hidden. Check the view options in the bottom-left if they allow for showing hidden nodes.</li>
<li>Containers view only: you're not running Docker, or you don't have any containers.</li>
<li>We haven't received any reports from probes recently.
Are the probes properly configured?</li>
<li>There are nodes, but they're currently hidden. Check the view options
in the bottom-left if they allow for showing hidden nodes.</li>
<li>Containers view only: you're not running Docker,
or you don't have any containers.</li>
</ul>
</NodesError>
);
@@ -217,7 +213,7 @@ export default class NodesChart extends React.Component {
const scale = this.state.scale;
const translate = this.state.panTranslate;
const transform = 'translate(' + translate + ') scale(' + scale + ')';
const transform = `translate(${translate}) scale(${scale})`;
const svgClassNames = this.state.maxNodesExceeded || nodeElements.size === 0 ? 'hide' : '';
const errorEmpty = this.renderEmptyTopologyError(AppStore.isTopologyEmpty());
const errorMaxNodesExceeded = this.renderMaxNodesError(this.state.maxNodesExceeded);
@@ -226,9 +222,10 @@ export default class NodesChart extends React.Component {
<div className="nodes-chart">
{errorEmpty}
{errorMaxNodesExceeded}
<svg width="100%" height="100%" id="nodes-chart-canvas" className={svgClassNames} onClick={this.handleMouseClick}>
<svg width="100%" height="100%" id="nodes-chart-canvas"
className={svgClassNames} onClick={this.handleMouseClick}>
<g transform="translate(24,24) scale(0.25)">
<Logo/>
<Logo />
</g>
<g className="canvas" transform={transform}>
<g className="edges">
@@ -244,30 +241,28 @@ export default class NodesChart extends React.Component {
}
initNodes(topology) {
return topology.map((node, id) => {
// copy relevant fields to state nodes
return makeMap({
id: id,
label: node.get('label_major'),
pseudo: node.get('pseudo'),
subLabel: node.get('label_minor'),
nodeCount: node.get('node_count'),
rank: node.get('rank'),
shape: node.get('shape'),
stack: node.get('stack'),
x: 0,
y: 0
});
});
// copy relevant fields to state nodes
return topology.map((node, id) => makeMap({
id,
label: node.get('label_major'),
pseudo: node.get('pseudo'),
subLabel: node.get('label_minor'),
nodeCount: node.get('node_count'),
rank: node.get('rank'),
shape: node.get('shape'),
stack: node.get('stack'),
x: 0,
y: 0
}));
}
initEdges(topology, stateNodes) {
let edges = makeMap();
topology.forEach(function(node, nodeId) {
topology.forEach((node, nodeId) => {
const adjacency = node.get('adjacency');
if (adjacency) {
adjacency.forEach(function(adjacent) {
adjacency.forEach(adjacent => {
const edge = [nodeId, adjacent];
const edgeId = edge.join(EDGE_ID_SEPARATOR);
@@ -282,8 +277,8 @@ export default class NodesChart extends React.Component {
edges = edges.set(edgeId, makeMap({
id: edgeId,
value: 1,
source: source,
target: target
source,
target
}));
}
});
@@ -305,7 +300,7 @@ export default class NodesChart extends React.Component {
const adjacency = AppStore.getAdjacentNodes(props.selectedNodeId);
const adjacentLayoutNodeIds = [];
adjacency.forEach(function(adjacentId) {
adjacency.forEach(adjacentId => {
// filter loopback
if (adjacentId !== props.selectedNodeId) {
adjacentLayoutNodeIds.push(adjacentId);
@@ -315,7 +310,8 @@ export default class NodesChart extends React.Component {
// move origin node to center of viewport
const zoomScale = state.scale;
const translate = state.panTranslate;
const centerX = (-translate[0] + (props.width + MARGINS.left - DETAILS_PANEL_WIDTH) / 2) / zoomScale;
const centerX = (-translate[0] + (props.width + MARGINS.left
- DETAILS_PANEL_WIDTH) / 2) / zoomScale;
const centerY = (-translate[1] + (props.height + MARGINS.top) / 2) / zoomScale;
stateNodes = stateNodes.mergeIn([props.selectedNodeId], {
x: centerX,
@@ -379,12 +375,10 @@ export default class NodesChart extends React.Component {
this.zoom.scale(state.scale);
this.zoom.translate(state.panTranslate);
const nodes = state.nodes.map(node => {
return node.merge({
x: node.get('px'),
y: node.get('py')
});
});
const nodes = state.nodes.map(node => node.merge({
x: node.get('px'),
y: node.get('py')
}));
const edges = state.edges.map(edge => {
if (edge.has('ppoints')) {
@@ -422,7 +416,7 @@ export default class NodesChart extends React.Component {
const timedLayouter = timely(doLayout);
const graph = timedLayouter(stateNodes, stateEdges, options);
log('graph layout took ' + timedLayouter.time + 'ms');
log(`graph layout took ${timedLayouter.time}ms`);
// layout was aborted
if (!graph) {
@@ -432,15 +426,11 @@ export default class NodesChart extends React.Component {
stateEdges = graph.edges;
// save coordinates for restore
stateNodes = stateNodes.map(node => {
return node.merge({
px: node.get('x'),
py: node.get('y')
});
});
stateEdges = stateEdges.map(edge => {
return edge.set('ppoints', edge.get('points'));
});
stateNodes = stateNodes.map(node => node.merge({
px: node.get('x'),
py: node.get('y')
}));
stateEdges = stateEdges.map(edge => edge.set('ppoints', edge.get('points')));
// adjust layout based on viewport
const xFactor = (props.width - MARGINS.left - MARGINS.right) / graph.width;
@@ -458,7 +448,7 @@ export default class NodesChart extends React.Component {
nodes: stateNodes,
edges: stateEdges,
scale: zoomScale,
nodeScale: nodeScale,
nodeScale,
maxNodesExceeded: false
};
}

View File

@@ -1,20 +1,18 @@
import React from 'react';
export default class NodesError extends React.Component {
render() {
let classNames = 'nodes-chart-error';
if (this.props.hidden) {
classNames += ' hide';
}
const iconClassName = 'fa ' + this.props.faIconClass;
return (
<div className={classNames}>
<div className="nodes-chart-error-icon">
<span className={iconClassName} />
</div>
{this.props.children}
</div>
);
export default function NodesError({children, faIconClass, hidden}) {
let classNames = 'nodes-chart-error';
if (hidden) {
classNames += ' hide';
}
const iconClassName = `fa ${faIconClass}`;
return (
<div className={classNames}>
<div className="nodes-chart-error-icon">
<span className={iconClassName} />
</div>
{children}
</div>
);
}

View File

@@ -41,7 +41,7 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) {
let edges = imEdges;
if (nodes.size > MAX_NODES) {
log('Too many nodes for graph layout engine. Limit: ' + MAX_NODES);
log(`Too many nodes for graph layout engine. Limit: ${MAX_NODES}`);
return null;
}
@@ -54,8 +54,8 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) {
// configure node margins
graph.setGraph({
nodesep: nodesep,
ranksep: ranksep
nodesep,
ranksep
});
// add nodes to the graph if not already there
@@ -140,6 +140,7 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) {
* @return {Object} modified layout
*/
function layoutSingleNodes(layout, opts) {
const result = Object.assign({}, layout);
const options = opts || {};
const margins = options.margins || DEFAULT_MARGINS;
const scale = options.scale || DEFAULT_SCALE;
@@ -203,12 +204,12 @@ function layoutSingleNodes(layout, opts) {
});
// adjust layout dimensions if graph is now bigger
layout.width = Math.max(layout.width, singleX + nodeWidth / 2 + nodesep);
layout.height = Math.max(layout.height, singleY + nodeHeight / 2 + ranksep);
layout.nodes = nodes;
result.width = Math.max(layout.width, singleX + nodeWidth / 2 + nodesep);
result.height = Math.max(layout.height, singleY + nodeHeight / 2 + ranksep);
result.nodes = nodes;
}
return layout;
return result;
}
/**
@@ -218,6 +219,7 @@ function layoutSingleNodes(layout, opts) {
* @return {Object} modified layout
*/
function shiftLayoutToCenter(layout, opts) {
const result = Object.assign({}, layout);
const options = opts || {};
const margins = options.margins || DEFAULT_MARGINS;
const width = options.width || DEFAULT_WIDTH;
@@ -233,14 +235,12 @@ function shiftLayoutToCenter(layout, opts) {
offsetY = (height - layout.height) / 2 + margins.top;
}
layout.nodes = layout.nodes.map(node => {
return node.merge({
x: node.get('x') + offsetX,
y: node.get('y') + offsetY
});
});
result.nodes = layout.nodes.map(node => node.merge({
x: node.get('x') + offsetX,
y: node.get('y') + offsetY
}));
layout.edges = layout.edges.map(edge => {
result.edges = layout.edges.map(edge => {
const points = edge.get('points').map(point => ({
x: point.x + offsetX,
y: point.y + offsetY
@@ -248,7 +248,7 @@ function shiftLayoutToCenter(layout, opts) {
return edge.set('points', points);
});
return layout;
return result;
}
/**
@@ -320,16 +320,16 @@ function cloneLayout(layout, nodes, edges) {
* @return {Object} modified layout
*/
function copyLayoutProperties(layout, nodeCache, edgeCache) {
layout.nodes = layout.nodes.map(node => {
return node.merge(nodeCache.get(node.get('id')));
});
layout.edges = layout.edges.map(edge => {
if (edgeCache.has(edge.get('id')) && hasSameEndpoints(edgeCache.get(edge.get('id')), layout.nodes)) {
const result = Object.assign({}, layout);
result.nodes = layout.nodes.map(node => node.merge(nodeCache.get(node.get('id'))));
result.edges = layout.edges.map(edge => {
if (edgeCache.has(edge.get('id'))
&& hasSameEndpoints(edgeCache.get(edge.get('id')), result.nodes)) {
return edge.merge(edgeCache.get(edge.get('id')));
}
return setSimpleEdgePoints(edge, nodeCache);
});
return layout;
return result;
}
/**
@@ -361,7 +361,8 @@ export function doLayout(immNodes, immEdges, opts) {
let layout;
++layoutRuns;
if (!options.forceRelayout && 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

@@ -28,7 +28,8 @@ describe('NodeDetails', () => {
it('shows n/a when node was not found', () => {
const c = TestUtils.renderIntoDocument(<NodeDetails notFound />);
const notFound = TestUtils.findRenderedDOMComponentWithClass(c, 'node-details-header-notavailable');
const notFound = TestUtils.findRenderedDOMComponentWithClass(c,
'node-details-header-notavailable');
expect(notFound).toBeDefined();
});

View File

@@ -99,7 +99,8 @@ export default class App extends React.Component {
<Logo />
</svg>
</div>
<Topologies topologies={this.state.topologies} currentTopology={this.state.currentTopology} />
<Topologies topologies={this.state.topologies}
currentTopology={this.state.currentTopology} />
</div>
<Nodes nodes={this.state.nodes} highlightedNodeIds={this.state.highlightedNodeIds}

View File

@@ -1,3 +1,4 @@
/* eslint react/jsx-no-bind: "off" */
import React from 'react';
import _ from 'lodash';
@@ -11,36 +12,28 @@ const SHAPES = ['circle', 'hexagon', 'square', 'heptagon'];
const NODE_COUNTS = [1, 2, 3];
const STACK_VARIANTS = [true, false];
const sample = function(collection) {
return _.range(_.random(4)).map(() => _.sample(collection));
};
const sample = (collection) => _.range(_.random(4)).map(() => _.sample(collection));
const deltaAdd = function(name, adjacency = [], shape = 'circle', stack = false, nodeCount = 1) {
return {
'adjacency': adjacency,
'controls': {},
'shape': shape,
'stack': stack,
'node_count': nodeCount,
'id': name,
'label_major': name,
'label_minor': 'weave-1',
'latest': {},
'metadata': {},
'origins': [],
'rank': 'alpine'
};
};
const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCount = 1) => ({
adjacency,
controls: {},
shape,
stack,
node_count: nodeCount,
id: name,
label_major: name,
label_minor: 'weave-1',
latest: {},
metadata: {},
origins: [],
rank: 'alpine'
});
function addAllVariants() {
const newNodes = _.flattenDeep(SHAPES.map(s => {
return STACK_VARIANTS.map(stack => {
if (!stack) return [deltaAdd([s, 1, stack].join('-'), [], s, stack, 1)];
return NODE_COUNTS.map(n => {
return deltaAdd([s, n, stack].join('-'), [], s, stack, n);
});
});
}));
const newNodes = _.flattenDeep(SHAPES.map(s => STACK_VARIANTS.map(stack => {
if (!stack) return [deltaAdd([s, 1, stack].join('-'), [], s, stack, 1)];
return NODE_COUNTS.map(n => deltaAdd([s, n, stack].join('-'), [], s, stack, n));
})));
receiveNodesDelta({
add: newNodes
@@ -50,7 +43,7 @@ function addAllVariants() {
function addNodes(n) {
const ns = AppStore.getNodes();
const nodeNames = ns.keySeq().toJS();
const newNodeNames = _.range(ns.size, ns.size + n).map((i) => 'zing' + i);
const newNodeNames = _.range(ns.size, ns.size + n).map((i) => `zing${i}`);
const allNodes = _(nodeNames).concat(newNodeNames).value();
receiveNodesDelta({

View File

@@ -2,21 +2,14 @@ import React from 'react';
import DetailsCard from './details-card';
export default class Details extends React.Component {
export default function Details({controlStatus, details, nodes}) {
// render all details as cards, later cards go on top
render() {
const details = this.props.details.toIndexedSeq();
return (
<div className="details">
{details.map((obj, index) => {
return (
<DetailsCard key={obj.id} index={index} cardCount={details.size}
nodes={this.props.nodes}
nodeControlStatus={this.props.controlStatus[obj.id]} {...obj} />
);
})}
</div>
);
}
return (
<div className="details">
{details.toIndexedSeq().map((obj, index) => <DetailsCard key={obj.id}
index={index} cardCount={details.size} nodes={nodes}
nodeControlStatus={controlStatus[obj.id]} {...obj} />
)}
</div>
);
}

View File

@@ -2,7 +2,8 @@ import React from 'react';
import { getNodeColor, getNodeColorDark } from '../utils/color-utils';
import Terminal from './terminal';
import { DETAILS_PANEL_WIDTH, DETAILS_PANEL_MARGINS, DETAILS_PANEL_OFFSET } from '../constants/styles';
import { DETAILS_PANEL_WIDTH, DETAILS_PANEL_MARGINS,
DETAILS_PANEL_OFFSET } from '../constants/styles';
export default function EmeddedTerminal({pipe, nodeId, details}) {
const node = details.get(nodeId);

View File

@@ -7,21 +7,25 @@ import { clickDownloadGraph, clickForceRelayout, clickPauseUpdate,
clickResumeUpdate } from '../actions/app-actions';
import { basePathSlash } from '../utils/web-api-utils';
export default (props) => {
export default function Footer(props) {
const { hostname, updatePaused, updatePausedAt, version } = props;
const contrastMode = isContrastMode();
// link url to switch contrast with current UI state
const otherContrastModeUrl = contrastMode ? basePathSlash(window.location.pathname) : contrastModeUrl;
const otherContrastModeTitle = contrastMode ? 'Switch to normal contrast' : 'Switch to high contrast';
const forceRelayoutTitle = 'Force re-layout (might reduce edge crossings, but may shift nodes around)';
const otherContrastModeUrl = contrastMode
? basePathSlash(window.location.pathname) : contrastModeUrl;
const otherContrastModeTitle = contrastMode
? 'Switch to normal contrast' : 'Switch to high contrast';
const forceRelayoutTitle = 'Force re-layout (might reduce edge crossings, '
+ 'but may shift nodes around)';
// pause button
const isPaused = updatePaused;
const updateCount = getUpdateBufferSize();
const hasUpdates = updateCount > 0;
const pausedAgo = moment(updatePausedAt).fromNow();
const pauseTitle = isPaused ? `Paused ${pausedAgo}` : 'Pause updates (freezes the nodes in their current layout)';
const pauseTitle = isPaused
? `Paused ${pausedAgo}` : 'Pause updates (freezes the nodes in their current layout)';
const pauseAction = isPaused ? clickResumeUpdate : clickPauseUpdate;
const pauseClassName = isPaused ? 'footer-icon footer-icon-active' : 'footer-icon';
let pauseLabel = '';
@@ -64,4 +68,4 @@ export default (props) => {
</div>
);
};
}

View File

@@ -1,59 +1,58 @@
/* eslint max-len: "off" */
import React from 'react';
export default class Logo extends React.Component {
render() {
return (
<g className="logo">
<path fill="#32324B" d="M114.937,118.165l75.419-67.366c-5.989-4.707-12.71-8.52-19.981-11.211l-55.438,49.52V118.165z"/>
<path fill="#32324B" d="M93.265,108.465l-20.431,18.25c1.86,7.57,4.88,14.683,8.87,21.135l11.561-10.326V108.465z"/>
<path fill="#00D2FF" d="M155.276,53.074V35.768C151.815,35.27,148.282,35,144.685,35c-3.766,0-7.465,0.286-11.079,0.828v36.604
L155.276,53.074z"/>
<path fill="#00D2FF" d="M155.276,154.874V82.133l-21.671,19.357v80.682c3.614,0.543,7.313,0.828,11.079,0.828
c4.41,0,8.723-0.407,12.921-1.147l58.033-51.838c1.971-6.664,3.046-13.712,3.046-21.015c0-3.439-0.254-6.817-0.708-10.132
L155.276,154.874z"/>
<path fill="#FF4B19" d="M155.276,133.518l58.14-51.933c-2.77-6.938-6.551-13.358-11.175-19.076l-46.965,41.951V133.518z"/>
<path fill="#FF4B19" d="M133.605,123.817l-18.668,16.676V41.242c-8.086,3.555-15.409,8.513-21.672,14.567V162.19
c4.885,4.724,10.409,8.787,16.444,12.03l23.896-21.345V123.817z"/>
<polygon fill="#32324B" points="325.563,124.099 339.389,72.22 357.955,72.22 337.414,144.377 315.556,144.377 303.311,95.79
291.065,144.377 269.207,144.377 248.666,72.22 267.232,72.22 281.058,124.099 294.752,72.22 311.869,72.22 "/>
<path fill="#32324B" d="M426.429,120.676c-2.106,14.352-13.167,24.623-32.128,24.623c-20.146,0-35.025-12.114-35.025-36.605
c0-24.622,15.406-37.395,35.025-37.395c21.726,0,33.182,15.933,33.182,37.263v3.819h-49.772c0,8.031,3.291,18.17,16.327,18.17
c7.242,0,12.904-3.555,14.353-10.27L426.429,120.676z M408.654,99.608c-0.659-10.008-7.11-13.694-14.484-13.694
c-8.427,0-14.879,5.135-15.801,13.694H408.654z"/>
<path fill="#32324B" d="M480.628,97.634v-2.502c0-5.662-2.37-9.351-13.036-9.351c-13.298,0-13.694,7.375-13.694,9.877h-17.117
c0-10.666,4.477-24.359,31.338-24.359c25.676,0,30.285,12.771,30.285,23.174v39.766c0,2.897,0.131,5.267,0.395,7.11l0.527,3.028
h-18.172v-7.241c-5.134,5.134-12.245,8.163-22.384,8.163c-14.221,0-25.018-8.296-25.018-22.648c0-16.59,15.67-20.146,21.99-21.199
L480.628,97.634z M480.628,111.195l-6.979,1.054c-3.819,0.658-8.427,1.315-11.192,1.843c-3.029,0.527-5.662,1.186-7.637,2.765
c-1.844,1.449-2.765,3.425-2.765,5.926c0,2.107,0.79,8.69,10.666,8.69c5.793,0,10.928-2.105,13.693-4.872
c3.556-3.555,4.214-8.032,4.214-11.587V111.195z"/>
<polygon fill="#32324B" points="549.495,144.377 525.399,144.377 501.698,72.221 521.186,72.221 537.775,127.392 554.499,72.221
573.459,72.221 "/>
<path fill="#32324B" d="M641.273,120.676c-2.106,14.352-13.167,24.623-32.128,24.623c-20.146,0-35.025-12.114-35.025-36.605
c0-24.622,15.406-37.395,35.025-37.395c21.726,0,33.182,15.933,33.182,37.263v3.819h-49.772c0,8.031,3.291,18.17,16.327,18.17
c7.242,0,12.904-3.555,14.354-10.27L641.273,120.676z M623.498,99.608c-0.659-10.008-7.109-13.694-14.483-13.694
c-8.428,0-14.88,5.135-15.802,13.694H623.498z"/>
<path fill="#32324B" d="M682.976,80.873c-7.524,0-16.896,2.376-16.896,10.692c0,17.952,46.201,1.452,46.201,30.229
c0,9.637-5.676,22.309-30.229,22.309c-19.009,0-27.721-9.636-28.249-22.44h11.881c0.264,7.788,5.147,13.332,17.688,13.332
c14.52,0,17.952-6.204,17.952-12.54c0-13.332-24.421-7.788-37.753-15.181c-4.885-2.771-8.316-7.128-8.316-15.048
c0-11.616,10.824-20.461,27.853-20.461c20.989,0,27.193,12.145,27.589,20.196h-11.484
C698.685,83.381,691.556,80.873,682.976,80.873z"/>
<path fill="#32324B" d="M756.233,134.994c10.429,0,17.953-5.939,19.009-16.632h10.957c-1.98,17.028-13.597,25.74-29.966,25.74
c-18.744,0-32.076-12.012-32.076-35.905c0-23.76,13.464-36.433,32.209-36.433c16.104,0,27.721,8.712,29.568,25.213h-10.956
c-1.452-11.353-9.24-16.104-18.877-16.104c-12.012,0-20.856,8.448-20.856,27.324C735.245,127.471,744.485,134.994,756.233,134.994z
"/>
<path fill="#32324B" d="M830.418,144.103c-19.141,0-32.341-12.145-32.341-36.169c0-23.893,13.2-36.169,32.341-36.169
c19.009,0,32.209,12.145,32.209,36.169C862.627,132.091,849.427,144.103,830.418,144.103z M830.418,134.994
c12.145,0,21.12-7.392,21.12-27.061c0-19.536-8.976-27.061-21.12-27.061c-12.276,0-21.253,7.393-21.253,27.061
C809.165,127.603,818.142,134.994,830.418,134.994z"/>
<path fill="#32324B" d="M888.629,72.688v10.692c3.96-6.732,12.54-11.616,22.969-11.616c19.009,0,30.757,12.673,30.757,36.169
c0,23.629-12.145,36.169-31.152,36.169c-10.429,0-18.745-4.224-22.573-11.22v35.641h-10.824V72.688H888.629z M910.409,134.994
c12.145,0,20.857-7.392,20.857-27.061c0-19.536-8.713-27.061-20.857-27.061c-12.275,0-21.912,7.393-21.912,27.061
C888.497,127.603,898.134,134.994,910.409,134.994z"/>
<path fill="#32324B" d="M1016.801,119.022c-1.452,12.408-10.032,25.08-30.229,25.08c-18.745,0-32.341-12.804-32.341-36.037
c0-21.912,13.464-36.301,32.209-36.301c19.8,0,30.757,14.784,30.757,38.018h-51.878c0.265,13.332,5.809,25.212,21.385,25.212
c11.484,0,18.217-7.128,19.141-16.104L1016.801,119.022z M1005.448,101.201c-1.056-14.916-9.636-20.328-19.272-20.328
c-10.824,0-19.141,7.26-20.46,20.328H1005.448z"/>
</g>
);
}
export default function Logo() {
return (
<g className="logo">
<path fill="#32324B" d="M114.937,118.165l75.419-67.366c-5.989-4.707-12.71-8.52-19.981-11.211l-55.438,49.52V118.165z" />
<path fill="#32324B" d="M93.265,108.465l-20.431,18.25c1.86,7.57,4.88,14.683,8.87,21.135l11.561-10.326V108.465z" />
<path fill="#00D2FF" d="M155.276,53.074V35.768C151.815,35.27,148.282,35,144.685,35c-3.766,0-7.465,0.286-11.079,0.828v36.604
L155.276,53.074z" />
<path fill="#00D2FF" d="M155.276,154.874V82.133l-21.671,19.357v80.682c3.614,0.543,7.313,0.828,11.079,0.828
c4.41,0,8.723-0.407,12.921-1.147l58.033-51.838c1.971-6.664,3.046-13.712,3.046-21.015c0-3.439-0.254-6.817-0.708-10.132
L155.276,154.874z" />
<path fill="#FF4B19" d="M155.276,133.518l58.14-51.933c-2.77-6.938-6.551-13.358-11.175-19.076l-46.965,41.951V133.518z" />
<path fill="#FF4B19" d="M133.605,123.817l-18.668,16.676V41.242c-8.086,3.555-15.409,8.513-21.672,14.567V162.19
c4.885,4.724,10.409,8.787,16.444,12.03l23.896-21.345V123.817z" />
<polygon fill="#32324B" points="325.563,124.099 339.389,72.22 357.955,72.22 337.414,144.377 315.556,144.377 303.311,95.79
291.065,144.377 269.207,144.377 248.666,72.22 267.232,72.22 281.058,124.099 294.752,72.22 311.869,72.22 " />
<path fill="#32324B" d="M426.429,120.676c-2.106,14.352-13.167,24.623-32.128,24.623c-20.146,0-35.025-12.114-35.025-36.605
c0-24.622,15.406-37.395,35.025-37.395c21.726,0,33.182,15.933,33.182,37.263v3.819h-49.772c0,8.031,3.291,18.17,16.327,18.17
c7.242,0,12.904-3.555,14.353-10.27L426.429,120.676z M408.654,99.608c-0.659-10.008-7.11-13.694-14.484-13.694
c-8.427,0-14.879,5.135-15.801,13.694H408.654z" />
<path fill="#32324B" d="M480.628,97.634v-2.502c0-5.662-2.37-9.351-13.036-9.351c-13.298,0-13.694,7.375-13.694,9.877h-17.117
c0-10.666,4.477-24.359,31.338-24.359c25.676,0,30.285,12.771,30.285,23.174v39.766c0,2.897,0.131,5.267,0.395,7.11l0.527,3.028
h-18.172v-7.241c-5.134,5.134-12.245,8.163-22.384,8.163c-14.221,0-25.018-8.296-25.018-22.648c0-16.59,15.67-20.146,21.99-21.199
L480.628,97.634z M480.628,111.195l-6.979,1.054c-3.819,0.658-8.427,1.315-11.192,1.843c-3.029,0.527-5.662,1.186-7.637,2.765
c-1.844,1.449-2.765,3.425-2.765,5.926c0,2.107,0.79,8.69,10.666,8.69c5.793,0,10.928-2.105,13.693-4.872
c3.556-3.555,4.214-8.032,4.214-11.587V111.195z" />
<polygon fill="#32324B" points="549.495,144.377 525.399,144.377 501.698,72.221 521.186,72.221 537.775,127.392 554.499,72.221
573.459,72.221 " />
<path fill="#32324B" d="M641.273,120.676c-2.106,14.352-13.167,24.623-32.128,24.623c-20.146,0-35.025-12.114-35.025-36.605
c0-24.622,15.406-37.395,35.025-37.395c21.726,0,33.182,15.933,33.182,37.263v3.819h-49.772c0,8.031,3.291,18.17,16.327,18.17
c7.242,0,12.904-3.555,14.354-10.27L641.273,120.676z M623.498,99.608c-0.659-10.008-7.109-13.694-14.483-13.694
c-8.428,0-14.88,5.135-15.802,13.694H623.498z" />
<path fill="#32324B" d="M682.976,80.873c-7.524,0-16.896,2.376-16.896,10.692c0,17.952,46.201,1.452,46.201,30.229
c0,9.637-5.676,22.309-30.229,22.309c-19.009,0-27.721-9.636-28.249-22.44h11.881c0.264,7.788,5.147,13.332,17.688,13.332
c14.52,0,17.952-6.204,17.952-12.54c0-13.332-24.421-7.788-37.753-15.181c-4.885-2.771-8.316-7.128-8.316-15.048
c0-11.616,10.824-20.461,27.853-20.461c20.989,0,27.193,12.145,27.589,20.196h-11.484
C698.685,83.381,691.556,80.873,682.976,80.873z" />
<path fill="#32324B" d="M756.233,134.994c10.429,0,17.953-5.939,19.009-16.632h10.957c-1.98,17.028-13.597,25.74-29.966,25.74
c-18.744,0-32.076-12.012-32.076-35.905c0-23.76,13.464-36.433,32.209-36.433c16.104,0,27.721,8.712,29.568,25.213h-10.956
c-1.452-11.353-9.24-16.104-18.877-16.104c-12.012,0-20.856,8.448-20.856,27.324C735.245,127.471,744.485,134.994,756.233,134.994z
" />
<path fill="#32324B" d="M830.418,144.103c-19.141,0-32.341-12.145-32.341-36.169c0-23.893,13.2-36.169,32.341-36.169
c19.009,0,32.209,12.145,32.209,36.169C862.627,132.091,849.427,144.103,830.418,144.103z M830.418,134.994
c12.145,0,21.12-7.392,21.12-27.061c0-19.536-8.976-27.061-21.12-27.061c-12.276,0-21.253,7.393-21.253,27.061
C809.165,127.603,818.142,134.994,830.418,134.994z" />
<path fill="#32324B" d="M888.629,72.688v10.692c3.96-6.732,12.54-11.616,22.969-11.616c19.009,0,30.757,12.673,30.757,36.169
c0,23.629-12.145,36.169-31.152,36.169c-10.429,0-18.745-4.224-22.573-11.22v35.641h-10.824V72.688H888.629z M910.409,134.994
c12.145,0,20.857-7.392,20.857-27.061c0-19.536-8.713-27.061-20.857-27.061c-12.275,0-21.912,7.393-21.912,27.061
C888.497,127.603,898.134,134.994,910.409,134.994z" />
<path fill="#32324B" d="M1016.801,119.022c-1.452,12.408-10.032,25.08-30.229,25.08c-18.745,0-32.341-12.804-32.341-36.037
c0-21.912,13.464-36.301,32.209-36.301c19.8,0,30.757,14.784,30.757,38.018h-51.878c0.265,13.332,5.809,25.212,21.385,25.212
c11.484,0,18.217-7.128,19.141-16.104L1016.801,119.022z M1005.448,101.201c-1.056-14.916-9.636-20.328-19.272-20.328
c-10.824,0-19.141,7.26-20.46,20.328H1005.448z" />
</g>
);
}

View File

@@ -44,7 +44,8 @@ export default class NodeDetails extends React.Component {
return (
<div className="node-details-tools-wrapper">
<div className="node-details-tools">
{showSwitchTopology && <span title={topologyTitle} className="fa fa-exchange" onClick={this.handleShowTopologyForNode} />}
{showSwitchTopology && <span title={topologyTitle}
className="fa fa-exchange" onClick={this.handleShowTopologyForNode} />}
<span title="Close details" className="fa fa-close" onClick={this.handleClickClose} />
</div>
</div>
@@ -60,7 +61,7 @@ export default class NodeDetails extends React.Component {
const tools = this.renderTools();
const styles = {
header: {
'backgroundColor': nodeColor
backgroundColor: nodeColor
}
};
@@ -103,7 +104,8 @@ export default class NodeDetails extends React.Component {
</div>
<div className="node-details-content">
<p className="node-details-content-info">
<strong>{this.props.label}</strong> is not visible to Scope when it is not communicating.
<strong>{this.props.label}</strong> is not visible to Scope when it
is not communicating.
Details will become available here when it communicates again.
</p>
</div>
@@ -113,7 +115,8 @@ export default class NodeDetails extends React.Component {
renderTable(table) {
const key = _.snakeCase(table.title);
return <NodeDetailsTable title={table.title} key={key} rows={table.rows} isNumeric={table.numeric} />;
return (<NodeDetailsTable title={table.title} key={key} rows={table.rows}
isNumeric={table.numeric} />);
}
render() {
@@ -136,10 +139,10 @@ export default class NodeDetails extends React.Component {
const tools = this.renderTools();
const styles = {
controls: {
'backgroundColor': brightenColor(nodeColor)
backgroundColor: brightenColor(nodeColor)
},
header: {
'backgroundColor': nodeColor
backgroundColor: nodeColor
}
};
@@ -174,23 +177,20 @@ export default class NodeDetails extends React.Component {
<NodeDetailsInfo rows={details.metadata} />
</div>}
{details.connections && details.connections.map(connections => {
return (
<div className="node-details-content-section" key={connections.id}>
<NodeDetailsTable {...connections} />
</div>
);
})}
{details.connections && details.connections.map(connections => <div
className="node-details-content-section" key={connections.id}>
<NodeDetailsTable {...connections} />
</div>
)}
{details.children && details.children.map(children => {
return (
<div className="node-details-content-section" key={children.topologyId}>
<NodeDetailsTable {...children} />
</div>
);
})}
{details.children && details.children.map(children => <div
className="node-details-content-section" key={children.topologyId}>
<NodeDetailsTable {...children} />
</div>
)}
{details.docker_labels && details.docker_labels.length > 0 && <div className="node-details-content-section">
{details.docker_labels && details.docker_labels.length > 0
&& <div className="node-details-content-section">
<div className="node-details-content-section-header">Docker Labels</div>
<NodeDetailsLabels rows={details.docker_labels} />
</div>}

View File

@@ -2,31 +2,25 @@ import React from 'react';
import NodeDetailsControlButton from './node-details-control-button';
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';
} else {
spinnerClassName += ' node-details-controls-spinner hide';
}
return (
<div className="node-details-controls">
{this.props.error && <div className="node-details-controls-error" title={this.props.error}>
<span className="node-details-controls-error-icon fa fa-warning" />
<span className="node-details-controls-error-messages">{this.props.error}</span>
</div>}
<span className="node-details-controls-buttons">
{this.props.controls && this.props.controls.map(control => {
return (
<NodeDetailsControlButton nodeId={this.props.nodeId} control={control}
pending={this.props.pending} key={control.id} />
);
})}
</span>
{this.props.controls && <span title="Applying..." className={spinnerClassName}></span>}
</div>
);
export default function NodeDetailsControls({controls, error, nodeId, pending}) {
let spinnerClassName = 'fa fa-circle-o-notch fa-spin';
if (pending) {
spinnerClassName += ' node-details-controls-spinner';
} else {
spinnerClassName += ' node-details-controls-spinner hide';
}
return (
<div className="node-details-controls">
{error && <div className="node-details-controls-error" title={error}>
<span className="node-details-controls-error-icon fa fa-warning" />
<span className="node-details-controls-error-messages">{error}</span>
</div>}
<span className="node-details-controls-buttons">
{controls && controls.map(control => <NodeDetailsControlButton
nodeId={nodeId} control={control} pending={pending} key={control.id} />)}
</span>
{controls && <span title="Applying..." className={spinnerClassName}></span>}
</div>
);
}

View File

@@ -4,19 +4,17 @@ import Sparkline from '../sparkline';
import metricFeeder from '../../hoc/metric-feeder';
import { formatMetric } from '../../utils/string-utils';
class NodeDetailsHealthItem extends React.Component {
render() {
return (
<div className="node-details-health-item">
<div className="node-details-health-item-value">{formatMetric(this.props.value, this.props)}</div>
<div className="node-details-health-item-sparkline">
<Sparkline data={this.props.samples} max={this.props.max}
first={this.props.first} last={this.props.last} />
</div>
<div className="node-details-health-item-label">{this.props.label}</div>
function NodeDetailsHealthItem(props) {
return (
<div className="node-details-health-item">
<div className="node-details-health-item-value">{formatMetric(props.value, props)}</div>
<div className="node-details-health-item-sparkline">
<Sparkline data={props.samples} max={props.max}
first={props.first} last={props.last} />
</div>
);
}
<div className="node-details-health-item-label">{props.label}</div>
</div>
);
}
export default metricFeeder(NodeDetailsHealthItem);

View File

@@ -3,15 +3,15 @@ import React from 'react';
import metricFeeder from '../../hoc/metric-feeder';
import { formatMetric } from '../../utils/string-utils';
class NodeDetailsHealthOverflowItem extends React.Component {
render() {
return (
<div className="node-details-health-overflow-item">
<div className="node-details-health-overflow-item-value">{formatMetric(this.props.value, this.props)}</div>
<div className="node-details-health-overflow-item-label truncate">{this.props.label}</div>
function NodeDetailsHealthOverflowItem(props) {
return (
<div className="node-details-health-overflow-item">
<div className="node-details-health-overflow-item-value">
{formatMetric(props.value, props)}
</div>
);
}
<div className="node-details-health-overflow-item-label truncate">{props.label}</div>
</div>
);
}
export default metricFeeder(NodeDetailsHealthOverflowItem);

View File

@@ -32,13 +32,11 @@ export default class NodeDetailsHealth extends React.Component {
return (
<div className="node-details-health" style={{flexWrap, justifyContent}}>
<div className="node-details-health-wrapper">
{primeMetrics.map(item => {
return <NodeDetailsHealthItem key={item.id} {...item} />;
})}
{primeMetrics.map(item => <NodeDetailsHealthItem key={item.id} {...item} />)}
{showOverflow && <NodeDetailsHealthOverflow items={overflowMetrics}
handleClick={() => this.handleClickMore()} />}
handleClick={this.handleClickMore} />}
</div>
<ShowMore handleClick={() => this.handleClickMore()} collection={this.props.metrics}
<ShowMore handleClick={this.handleClickMore} collection={this.props.metrics}
expanded={this.state.expanded} notShown={notShown} hideNumber />
</div>
);

View File

@@ -27,19 +27,16 @@ export default class NodeDetailsInfo extends React.Component {
}
return (
<div className="node-details-info">
{rows.map(field => {
return (
<div className="node-details-info-field" key={field.id}>
<div className="node-details-info-field-label truncate" title={field.label}>
{field.label}
</div>
<div className="node-details-info-field-value truncate" title={field.value}>
{field.value}
</div>
{rows.map(field => (<div className="node-details-info-field" key={field.id}>
<div className="node-details-info-field-label truncate" title={field.label}>
{field.label}
</div>
);
})}
<ShowMore handleClick={() => this.handleClickMore()} collection={this.props.rows}
<div className="node-details-info-field-value truncate" title={field.value}>
{field.value}
</div>
</div>
))}
<ShowMore handleClick={this.handleClickMore} collection={this.props.rows}
expanded={this.state.expanded} notShown={notShown} />
</div>
);

View File

@@ -1,22 +1,17 @@
import React from 'react';
export default class NodeDetailsLabels extends React.Component {
render() {
return (
<div className="node-details-labels">
{this.props.rows.map(field => {
return (
<div className="node-details-labels-field" key={field.id}>
<div className="node-details-labels-field-label truncate" title={field.label}>
{field.label}
</div>
<div className="node-details-labels-field-value truncate" title={field.value}>
{field.value}
</div>
</div>
);
})}
</div>
);
}
export default function NodeDetailsLabels({rows}) {
return (
<div className="node-details-labels">
{rows.map(field => (<div className="node-details-labels-field" key={field.id}>
<div className="node-details-labels-field-label truncate" title={field.label}>
{field.label}
</div>
<div className="node-details-labels-field-value truncate" title={field.value}>
{field.value}
</div>
</div>
))}
</div>
);
}

View File

@@ -16,13 +16,14 @@ export default class NodeDetailsRelatives extends React.Component {
handleLimitClick(ev) {
ev.preventDefault();
const limit = this.state.limit ? 0 : this.DEFAULT_LIMIT;
this.setState({limit: limit});
this.setState({limit});
}
render() {
let relatives = this.props.relatives;
const limited = this.state.limit > 0 && relatives.length > this.state.limit;
const showLimitAction = limited || (this.state.limit === 0 && relatives.length > this.DEFAULT_LIMIT);
const showLimitAction = limited || (this.state.limit === 0
&& relatives.length > this.DEFAULT_LIMIT);
const limitActionText = limited ? 'Show more' : 'Show less';
if (limited) {
relatives = relatives.slice(0, this.state.limit);
@@ -30,10 +31,9 @@ export default class NodeDetailsRelatives extends React.Component {
return (
<div className="node-details-relatives">
{relatives.map(relative => {
return <NodeDetailsRelativesLink {...relative} key={relative.id} />;
})}
{showLimitAction && <span className="node-details-relatives-more" onClick={this.handleLimitClick}>{limitActionText}</span>}
{relatives.map(relative => <NodeDetailsRelativesLink {...relative} key={relative.id} />)}
{showLimitAction && <span className="node-details-relatives-more"
onClick={this.handleLimitClick}>{limitActionText}</span>}
</div>
);
}

View File

@@ -2,14 +2,12 @@ import React from 'react';
import { formatMetric } from '../../utils/string-utils';
class NodeDetailsTableNodeMetric extends React.Component {
render() {
return (
<td className="node-details-table-node-metric">
{formatMetric(this.props.value, this.props)}
</td>
);
}
function NodeDetailsTableNodeMetric(props) {
return (
<td className="node-details-table-node-metric">
{formatMetric(props.value, props)}
</td>
);
}
export default NodeDetailsTableNodeMetric;

View File

@@ -25,7 +25,8 @@ export default class NodeDetailsTable extends React.Component {
handleHeaderClick(ev, headerId) {
ev.preventDefault();
const sortedDesc = headerId === this.state.sortBy ? !this.state.sortedDesc : this.state.sortedDesc;
const sortedDesc = headerId === this.state.sortBy
? !this.state.sortedDesc : this.state.sortedDesc;
const sortBy = headerId;
this.setState({sortBy, sortedDesc});
}
@@ -47,17 +48,15 @@ export default class NodeDetailsTable extends React.Component {
getMetaDataSorters() {
// returns an array of sorters that will take a node
return _.get(this.props.nodes, [0, 'metadata'], []).map((field, index) => {
return node => {
const nodeMetadataField = node.metadata[index];
if (nodeMetadataField) {
if (isNumberField(nodeMetadataField)) {
return parseFloat(nodeMetadataField.value);
}
return nodeMetadataField.value;
return _.get(this.props.nodes, [0, 'metadata'], []).map((field, index) => node => {
const nodeMetadataField = node.metadata[index];
if (nodeMetadataField) {
if (isNumberField(nodeMetadataField)) {
return parseFloat(nodeMetadataField.value);
}
return null;
};
return nodeMetadataField.value;
}
return null;
});
}
@@ -81,8 +80,9 @@ export default class NodeDetailsTable extends React.Component {
['metrics', 'metadata'].forEach(collection => {
if (node[collection]) {
node[collection].forEach(field => {
field.valueType = collection;
values[field.id] = field;
const result = Object.assign({}, field);
result.valueType = collection;
values[field.id] = result;
});
}
});
@@ -103,7 +103,8 @@ export default class NodeDetailsTable extends React.Component {
this.handleHeaderClick(ev, header.id);
};
// sort by first metric by default
const isSorted = this.state.sortBy !== null ? header.id === this.state.sortBy : header.id === defaultSortBy;
const isSorted = this.state.sortBy !== null
? header.id === this.state.sortBy : header.id === defaultSortBy;
const isSortedDesc = isSorted && this.state.sortedDesc;
const isSortedAsc = isSorted && !isSortedDesc;
if (isSorted) {
@@ -111,8 +112,10 @@ export default class NodeDetailsTable extends React.Component {
}
return (
<td className={headerClasses.join(' ')} onClick={onHeaderClick} key={header.id}>
{isSortedAsc && <span className="node-details-table-header-sorter fa fa-caret-up" />}
{isSortedDesc && <span className="node-details-table-header-sorter fa fa-caret-down" />}
{isSortedAsc
&& <span className="node-details-table-header-sorter fa fa-caret-up" />}
{isSortedDesc
&& <span className="node-details-table-header-sorter fa fa-caret-down" />}
{header.label}
</td>
);
@@ -145,7 +148,8 @@ export default class NodeDetailsTable extends React.Component {
render() {
const headers = this.renderHeaders();
let nodes = _.sortBy(this.props.nodes, this.getValueForSortBy, 'label', this.getMetaDataSorters());
let nodes = _.sortBy(this.props.nodes, this.getValueForSortBy, 'label',
this.getMetaDataSorters());
const limited = nodes && this.state.limit > 0 && nodes.length > this.state.limit;
const expanded = this.state.limit === 0;
const notShown = nodes.length - this.DEFAULT_LIMIT;
@@ -176,7 +180,8 @@ export default class NodeDetailsTable extends React.Component {
})}
</tbody>
</table>
<ShowMore handleClick={() => this.handleLimitClick()} collection={this.props.nodes} expanded={expanded} notShown={notShown} />
<ShowMore handleClick={this.handleLimitClick} collection={this.props.nodes}
expanded={expanded} notShown={notShown} />
</div>
);
}

View File

@@ -19,11 +19,11 @@ export default class ShowMore extends React.Component {
const limitActionIcon = !expanded && notShown > 0 ? 'fa fa-caret-down' : 'fa fa-caret-up';
if (!showLimitAction) {
return <span/>;
return <span />;
}
return (
<div className="show-more" onClick={this.handleClick}>
{limitActionText} <span className={'show-more-icon ' + limitActionIcon}/>
{limitActionText} <span className={`show-more-icon ${limitActionIcon}`} />
</div>
);
}

View File

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

View File

@@ -30,12 +30,10 @@ export default class Sparkline extends React.Component {
this.line.interpolate(this.props.interpolate);
// Convert dates into D3 dates
data = data.map(d => {
return {
date: parseDate(d.date),
value: d.value
};
});
data = data.map(d => ({
date: parseDate(d.date),
value: d.value
}));
// determine date range
let firstDate = this.props.first ? parseDate(this.props.first) : data[0].date;
@@ -51,16 +49,17 @@ export default class Sparkline extends React.Component {
// determine value range
const minValue = this.props.min !== undefined ? this.props.min : d3.min(data, d => d.value);
const maxValue = this.props.max !== undefined ? Math.max(this.props.max, d3.max(data, d => d.value)) : d3.max(data, d => d.value);
const maxValue = this.props.max !== undefined
? Math.max(this.props.max, d3.max(data, d => d.value)) : d3.max(data, d => d.value);
this.y.domain([minValue, maxValue]);
const lastValue = data[data.length - 1].value;
const lastX = this.x(lastDate);
const lastY = this.y(lastValue);
const title = 'Last ' + d3.round((lastDate - firstDate) / 1000) + ' seconds, ' +
data.length + ' samples, min: ' + d3.round(d3.min(data, d => d.value), 2) +
', max: ' + d3.round(d3.max(data, d => d.value), 2) +
', mean: ' + d3.round(d3.mean(data, d => d.value), 2);
const title = `Last ${d3.round((lastDate - firstDate) / 1000)} seconds, ` +
`${data.length} samples, min: ${d3.round(d3.min(data, d => d.value), 2)}` +
`, max: ${d3.round(d3.max(data, d => d.value), 2)}` +
`, mean: ${d3.round(d3.mean(data, d => d.value), 2)}`;
return {title, lastX, lastY, data};
}

View File

@@ -1,38 +1,36 @@
import React from 'react';
export default class Status extends React.Component {
render() {
let title = '';
let text = 'Trying to reconnect...';
let showWarningIcon = false;
let classNames = 'status sidebar-item';
export default function Status({errorUrl, topologiesLoaded, topology, websocketClosed}) {
let title = '';
let text = 'Trying to reconnect...';
let showWarningIcon = false;
let classNames = 'status sidebar-item';
if (this.props.errorUrl) {
title = `Cannot reach Scope. Make sure the following URL is reachable: ${this.props.errorUrl}`;
classNames += ' status-loading';
showWarningIcon = true;
} else if (!this.props.topologiesLoaded) {
text = 'Connecting to Scope...';
classNames += ' status-loading';
showWarningIcon = true;
} else if (this.props.websocketClosed) {
classNames += ' status-loading';
showWarningIcon = true;
} else if (this.props.topology) {
const stats = this.props.topology.get('stats');
text = `${stats.get('node_count')} nodes`;
if (stats.get('filtered_nodes')) {
text = `${text} (${stats.get('filtered_nodes')} filtered)`;
}
classNames += ' status-stats';
showWarningIcon = false;
if (errorUrl) {
title = `Cannot reach Scope. Make sure the following URL is reachable: ${errorUrl}`;
classNames += ' status-loading';
showWarningIcon = true;
} else if (!topologiesLoaded) {
text = 'Connecting to Scope...';
classNames += ' status-loading';
showWarningIcon = true;
} else if (websocketClosed) {
classNames += ' status-loading';
showWarningIcon = true;
} else if (topology) {
const stats = topology.get('stats');
text = `${stats.get('node_count')} nodes`;
if (stats.get('filtered_nodes')) {
text = `${text} (${stats.get('filtered_nodes')} filtered)`;
}
return (
<div className={classNames}>
{showWarningIcon && <span className="status-icon fa fa-exclamation-circle" />}
<span className="status-label" title={title}>{text}</span>
</div>
);
classNames += ' status-stats';
showWarningIcon = false;
}
return (
<div className={classNames}>
{showWarningIcon && <span className="status-icon fa fa-exclamation-circle" />}
<span className="status-label" title={title}>{text}</span>
</div>
);
}

View File

@@ -1,3 +1,4 @@
/* eslint no-return-assign: "off", react/jsx-no-bind: "off" */
import debug from 'debug';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -10,7 +11,7 @@ import { getPipeStatus, basePath } from '../utils/web-api-utils';
import Term from '../vendor/term.js';
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = wsProto + '://' + location.host + basePath(location.pathname);
const wsUrl = `${wsProto}://${location.host}${basePath(location.pathname)}`;
const log = debug('scope:terminal');
const DEFAULT_COLS = 80;
@@ -66,7 +67,7 @@ function openNewWindow(url, bcr, minWidth = 200) {
};
const windowOptionsString = Object.keys(windowOptions)
.map((k) => k + '=' + windowOptions[k])
.map((k) => `${k}=${windowOptions[k]}`)
.join(',');
window.open(url, '', windowOptionsString);
@@ -92,7 +93,7 @@ export default class Terminal extends React.Component {
}
createWebsocket(term) {
const socket = new WebSocket(wsUrl + '/api/pipe/' + this.getPipeId());
const socket = new WebSocket(`${wsUrl}/api/pipe/${this.getPipeId()}`);
socket.binaryType = 'arraybuffer';
getPipeStatus(this.getPipeId());
@@ -157,8 +158,8 @@ export default class Terminal extends React.Component {
this.resizeTimeout = setTimeout(() => {
this.setState({
pixelPerCol: pixelPerCol,
pixelPerRow: pixelPerRow
pixelPerCol,
pixelPerRow
});
this.handleResize();
}, 10);

View File

@@ -19,7 +19,8 @@ export default class Topologies extends React.Component {
const isActive = subTopology === this.props.currentTopology;
const topologyId = subTopology.get('id');
const title = this.renderTitle(subTopology);
const className = isActive ? 'topologies-sub-item topologies-sub-item-active' : 'topologies-sub-item';
const className = isActive
? 'topologies-sub-item topologies-sub-item-active' : 'topologies-sub-item';
return (
<div className={className} title={title} key={topologyId} rel={topologyId}
@@ -32,13 +33,14 @@ export default class Topologies extends React.Component {
}
renderTitle(topology) {
return ['Nodes: ' + topology.getIn(['stats', 'node_count']),
'Connections: ' + topology.getIn(['stats', 'node_count'])].join('\n');
return `Nodes: ${topology.getIn(['stats', 'node_count'])}\n`
+ `Connections: ${topology.getIn(['stats', 'node_count'])}`;
}
renderTopology(topology) {
const isActive = topology === this.props.currentTopology;
const className = isActive ? 'topologies-item-main topologies-item-main-active' : 'topologies-item-main';
const className = isActive
? 'topologies-item-main topologies-item-main-active' : 'topologies-item-main';
const topologyId = topology.get('id');
const title = this.renderTitle(topology);
@@ -50,7 +52,8 @@ export default class Topologies extends React.Component {
</div>
</div>
<div className="topologies-sub">
{topology.has('sub_topologies') && topology.get('sub_topologies').map(this.renderSubTopology)}
{topology.has('sub_topologies')
&& topology.get('sub_topologies').map(this.renderSubTopology)}
</div>
</div>
);

View File

@@ -27,7 +27,7 @@ export default class TopologyOptions extends React.Component {
activeValue = activeOptions.get(option);
} else {
// get default value
items.forEach(function(item) {
items.forEach(item => {
if (item.get('default')) {
activeValue = item.get('value');
}
@@ -35,7 +35,7 @@ export default class TopologyOptions extends React.Component {
}
// render active option as text, add other options as actions
items.forEach(function(item) {
items.forEach(item => {
if (item.get('value') === activeValue) {
activeText = item.get('display');
} else {
@@ -62,9 +62,7 @@ export default class TopologyOptions extends React.Component {
return (
<div className="topology-options">
{options.toIndexedSeq().map(function(items) {
return this.renderOption(items);
}, this)}
{options.toIndexedSeq().map(items => this.renderOption(items))}
</div>
);
}

View File

@@ -5,11 +5,9 @@ const log = debug('scope:dispatcher');
const instance = new Dispatcher();
instance.dispatch = _.wrap(Dispatcher.prototype.dispatch, function(func) {
const args = Array.prototype.slice.call(arguments, 1);
const type = args[0] && args[0].type;
log(type, args[0]);
func.apply(this, args);
instance.dispatch = _.wrap(Dispatcher.prototype.dispatch, (func, payload) => {
log(payload.type, payload);
func.call(instance, payload);
});
export default instance;

View File

@@ -148,7 +148,8 @@ export default ComposedComponent => class extends React.Component {
.filter(dateFilter);
const lastValue = samples.length > 0 ? samples[samples.length - 1].value : null;
const slidingWindow = {first: movingFirstDate, last: movingLastDate, max, samples, value: lastValue};
const slidingWindow = {first: movingFirstDate,
last: movingLastDate, max, samples, value: lastValue};
return <ComposedComponent {...this.props} {...slidingWindow} />;
}

View File

@@ -4,9 +4,8 @@ jest.dontMock('../app-store');
// Appstore test suite using Jasmine matchers
describe('AppStore', function() {
describe('AppStore', () => {
const ActionTypes = require('../../constants/action-types').default;
let AppDispatcher;
let AppStore;
let registeredCallback;
@@ -150,22 +149,22 @@ describe('AppStore', function() {
state: {}
};
beforeEach(function() {
beforeEach(() => {
AppStore = require('../app-store').default;
AppDispatcher = AppStore.getDispatcher();
const AppDispatcher = AppStore.getDispatcher();
const callback = AppDispatcher.dispatch.bind(AppDispatcher);
registeredCallback = callback;
});
// topology tests
it('init with no topologies', function() {
it('init with no topologies', () => {
const topos = AppStore.getTopologies();
expect(topos.size).toBe(0);
expect(AppStore.getCurrentTopology()).toBeUndefined();
});
it('get current topology', function() {
it('get current topology', () => {
registeredCallback(ClickTopologyAction);
registeredCallback(ReceiveTopologiesAction);
@@ -175,7 +174,7 @@ describe('AppStore', function() {
expect(AppStore.getCurrentTopologyOptions().get('option1')).toBeDefined();
});
it('get sub-topology', function() {
it('get sub-topology', () => {
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickSubTopologyAction);
@@ -187,7 +186,7 @@ describe('AppStore', function() {
// topology options
it('changes topology option', function() {
it('changes topology option', () => {
// default options
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickTopologyAction);
@@ -211,11 +210,11 @@ describe('AppStore', function() {
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('off');
});
it('sets topology options from route', function() {
it('sets topology options from route', () => {
RouteAction.state = {
'topologyId': 'topo1',
'selectedNodeId': null,
'topologyOptions': {'topo1': {'option1': 'on'}}};
topologyId: 'topo1',
selectedNodeId: null,
topologyOptions: {topo1: {option1: 'on'}}};
registeredCallback(RouteAction);
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('on');
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('on');
@@ -227,11 +226,11 @@ describe('AppStore', function() {
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('on');
});
it('uses default topology options from route', function() {
it('uses default topology options from route', () => {
RouteAction.state = {
'topologyId': 'topo1',
'selectedNodeId': null,
'topologyOptions': null};
topologyId: 'topo1',
selectedNodeId: null,
topologyOptions: null};
registeredCallback(RouteAction);
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickTopologyAction);
@@ -241,7 +240,7 @@ describe('AppStore', function() {
// nodes delta
it('replaces adjacency on update', function() {
it('replaces adjacency on update', () => {
registeredCallback(ReceiveNodesDeltaAction);
expect(AppStore.getNodes().toJS().n1.adjacency).toEqual(['n1', 'n2']);
registeredCallback(ReceiveNodesDeltaUpdateAction);
@@ -250,18 +249,18 @@ describe('AppStore', function() {
// browsing
it('shows nodes that were received', function() {
it('shows nodes that were received', () => {
registeredCallback(ReceiveNodesDeltaAction);
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
});
it('knows a route was set', function() {
it('knows a route was set', () => {
expect(AppStore.isRouteSet()).toBeFalsy();
registeredCallback(RouteAction);
expect(AppStore.isRouteSet()).toBeTruthy();
});
it('gets selected node after click', function() {
it('gets selected node after click', () => {
registeredCallback(ReceiveNodesDeltaAction);
registeredCallback(ClickNodeAction);
@@ -273,7 +272,7 @@ describe('AppStore', function() {
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
});
it('keeps showing nodes on navigating back after node click', function() {
it('keeps showing nodes on navigating back after node click', () => {
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickTopologyAction);
registeredCallback(ReceiveNodesDeltaAction);
@@ -284,13 +283,13 @@ describe('AppStore', function() {
expect(AppStore.getAppState().selectedNodeId).toEqual('n1');
// go back in browsing
RouteAction.state = {'topologyId': 'topo1', 'selectedNodeId': null};
RouteAction.state = {topologyId: 'topo1', selectedNodeId: null};
registeredCallback(RouteAction);
expect(AppStore.getSelectedNodeId()).toBe(null);
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
});
it('closes details when changing topologies', function() {
it('closes details when changing topologies', () => {
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickTopologyAction);
registeredCallback(ReceiveNodesDeltaAction);
@@ -309,7 +308,7 @@ describe('AppStore', function() {
// connection errors
it('resets topology on websocket reconnect', function() {
it('resets topology on websocket reconnect', () => {
registeredCallback(ReceiveNodesDeltaAction);
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
@@ -326,7 +325,7 @@ describe('AppStore', function() {
// adjacency test
it('returns the correct adjacency set for a node', function() {
it('returns the correct adjacency set for a node', () => {
registeredCallback(ReceiveNodesDeltaAction);
expect(AppStore.getAdjacentNodes().size).toEqual(0);
@@ -341,7 +340,7 @@ describe('AppStore', function() {
// empty topology
it('detects that the topology is empty', function() {
it('detects that the topology is empty', () => {
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickTopologyAction);
expect(AppStore.isTopologyEmpty()).toBeFalsy();
@@ -355,7 +354,7 @@ describe('AppStore', function() {
// selection of relatives
it('keeps relatives as a stack', function() {
it('keeps relatives as a stack', () => {
registeredCallback(ClickNodeAction);
expect(AppStore.getSelectedNodeId()).toBe('n1');
expect(AppStore.getNodeDetails().size).toEqual(1);
@@ -377,7 +376,7 @@ describe('AppStore', function() {
expect(AppStore.getNodeDetails().has('rel1')).toBeFalsy();
});
it('keeps clears stack when sibling is clicked', function() {
it('keeps clears stack when sibling is clicked', () => {
registeredCallback(ClickNodeAction);
expect(AppStore.getSelectedNodeId()).toBe('n1');
expect(AppStore.getNodeDetails().size).toEqual(1);
@@ -400,7 +399,7 @@ describe('AppStore', function() {
expect(AppStore.getNodeDetails().has('rel1')).toBeFalsy();
});
it('selectes relatives topology while keeping node selected', function() {
it('selectes relatives topology while keeping node selected', () => {
registeredCallback(ClickTopologyAction);
registeredCallback(ReceiveTopologiesAction);
expect(AppStore.getCurrentTopology().get('name')).toBe('Topo1');

View File

@@ -62,12 +62,12 @@ const topologySorter = topology => topology.get('rank');
// map for easy lookup
function processTopologies(nextTopologies) {
// add IDs to topology objects in-place
updateTopologyIds(nextTopologies);
const topologiesWithId = updateTopologyIds(nextTopologies);
// cache URLs by ID
topologyUrlsById = setTopologyUrlsById(topologyUrlsById, nextTopologies);
topologyUrlsById = setTopologyUrlsById(topologyUrlsById, topologiesWithId);
const immNextTopologies = fromJS(nextTopologies).sortBy(topologySorter);
const immNextTopologies = fromJS(topologiesWithId).sortBy(topologySorter);
topologies = topologies.mergeDeep(immNextTopologies);
}
@@ -80,7 +80,7 @@ function setDefaultTopologyOptions(topologyList) {
topologyList.forEach(topology => {
let defaultOptions = makeOrderedMap();
if (topology.has('options')) {
topology.get('options').forEach(function(items, option) {
topology.get('options').forEach((items, option) => {
items.forEach(item => {
if (item.get('default') === true) {
defaultOptions = defaultOptions.set(option, item.get('value'));
@@ -106,9 +106,7 @@ function closeNodeDetails(nodeId) {
if (nodeDetails.size > 0) {
const popNodeId = nodeId || nodeDetails.keySeq().last();
// remove pipe if it belongs to the node being closed
controlPipes = controlPipes.filter(pipe => {
return pipe.get('nodeId') !== popNodeId;
});
controlPipes = controlPipes.filter(pipe => pipe.get('nodeId') !== popNodeId);
nodeDetails = nodeDetails.delete(popNodeId);
}
if (nodeDetails.size === 0 || selectedNodeId === nodeId) {
@@ -135,7 +133,7 @@ export class AppStore extends Store {
return {
controlPipe: this.getControlPipe(),
nodeDetails: this.getNodeDetailsState(),
selectedNodeId: selectedNodeId,
selectedNodeId,
topologyId: currentTopologyId,
topologyOptions: topologyOptions.toJS() // all options
};
@@ -152,7 +150,7 @@ export class AppStore extends Store {
if (nodes.has(nodeId)) {
adjacentNodes = makeSet(nodes.get(nodeId).get('adjacency'));
// fill up set with reverse edges
nodes.forEach(function(node, id) {
nodes.forEach((node, id) => {
if (node.get('adjacency') && node.get('adjacency').includes(nodeId)) {
adjacentNodes = adjacentNodes.add(id);
}
@@ -200,12 +198,10 @@ export class AppStore extends Store {
const adjacency = nodes.get(mouseOverNodeId).get('adjacency');
if (adjacency) {
return _.flatten(
adjacency.forEach(function(nodeId) {
return [
[nodeId, mouseOverNodeId].join(EDGE_ID_SEPARATOR),
[mouseOverNodeId, nodeId].join(EDGE_ID_SEPARATOR)
];
})
adjacency.map((nodeId) => [
[nodeId, mouseOverNodeId].join(EDGE_ID_SEPARATOR),
[mouseOverNodeId, nodeId].join(EDGE_ID_SEPARATOR)
]).toJS()
);
}
}
@@ -235,9 +231,9 @@ export class AppStore extends Store {
}
getNodeDetailsState() {
return nodeDetails.toIndexedSeq().map(details => {
return {id: details.id, label: details.label, topologyId: details.topologyId};
}).toJS();
return nodeDetails.toIndexedSeq().map(details => ({
id: details.id, label: details.label, topologyId: details.topologyId
})).toJS();
}
getTopCardNodeId() {
@@ -281,7 +277,8 @@ export class AppStore extends Store {
}
isTopologyEmpty() {
return currentTopology && currentTopology.get('stats') && currentTopology.get('stats').get('node_count') === 0 && nodes.size === 0;
return currentTopology && currentTopology.get('stats')
&& currentTopology.get('stats').get('node_count') === 0 && nodes.size === 0;
}
isUpdatePaused() {
@@ -298,327 +295,329 @@ export class AppStore extends Store {
}
switch (payload.type) {
case ActionTypes.CHANGE_TOPOLOGY_OPTION:
resumeUpdate();
if (topologyOptions.getIn([payload.topologyId, payload.option])
!== payload.value) {
nodes = nodes.clear();
}
topologyOptions = topologyOptions.setIn(
[payload.topologyId, payload.option],
payload.value
);
this.__emitChange();
break;
case ActionTypes.CLEAR_CONTROL_ERROR:
controlStatus = controlStatus.removeIn([payload.nodeId, 'error']);
this.__emitChange();
break;
case ActionTypes.CLICK_BACKGROUND:
closeAllNodeDetails();
this.__emitChange();
break;
case ActionTypes.CLICK_CLOSE_DETAILS:
closeNodeDetails(payload.nodeId);
this.__emitChange();
break;
case ActionTypes.CLICK_CLOSE_TERMINAL:
controlPipes = controlPipes.clear();
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;
// click on sibling closes all
closeAllNodeDetails();
// select new node if it's not the same (in that case just delesect)
if (prevDetailsStackSize > 1 || prevSelectedNodeId !== payload.nodeId) {
// dont set origin if a node was already selected, suppresses animation
const origin = prevSelectedNodeId === null ? payload.origin : null;
nodeDetails = nodeDetails.set(
payload.nodeId,
{
id: payload.nodeId,
label: payload.label,
origin,
topologyId: currentTopologyId
}
case ActionTypes.CHANGE_TOPOLOGY_OPTION: {
resumeUpdate();
if (topologyOptions.getIn([payload.topologyId, payload.option])
!== payload.value) {
nodes = nodes.clear();
}
topologyOptions = topologyOptions.setIn(
[payload.topologyId, payload.option],
payload.value
);
selectedNodeId = payload.nodeId;
}
this.__emitChange();
break;
case ActionTypes.CLICK_PAUSE_UPDATE:
updatePausedAt = new Date;
this.__emitChange();
break;
case ActionTypes.CLICK_RELATIVE:
if (nodeDetails.has(payload.nodeId)) {
// bring to front
const details = nodeDetails.get(payload.nodeId);
nodeDetails = nodeDetails.delete(payload.nodeId);
nodeDetails = nodeDetails.set(payload.nodeId, details);
} else {
nodeDetails = nodeDetails.set(
payload.nodeId,
{
id: payload.nodeId,
label: payload.label,
origin: payload.origin,
topologyId: payload.topologyId
}
);
}
this.__emitChange();
break;
case ActionTypes.CLICK_RESUME_UPDATE:
resumeUpdate();
this.__emitChange();
break;
case ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE:
resumeUpdate();
nodeDetails = nodeDetails.filter((v, k) => k === payload.nodeId);
controlPipes = controlPipes.clear();
selectedNodeId = payload.nodeId;
if (payload.topologyId !== currentTopologyId) {
setTopology(payload.topologyId);
nodes = nodes.clear();
}
this.__emitChange();
break;
case ActionTypes.CLICK_TOPOLOGY:
resumeUpdate();
closeAllNodeDetails();
if (payload.topologyId !== currentTopologyId) {
setTopology(payload.topologyId);
nodes = nodes.clear();
}
this.__emitChange();
break;
case ActionTypes.CLOSE_WEBSOCKET:
if (!websocketClosed) {
websocketClosed = true;
this.__emitChange();
break;
}
break;
case ActionTypes.DESELECT_NODE:
closeNodeDetails();
this.__emitChange();
break;
case ActionTypes.DO_CONTROL:
controlStatus = controlStatus.set(payload.nodeId, makeMap({
pending: true,
error: null
}));
this.__emitChange();
break;
case ActionTypes.ENTER_EDGE:
mouseOverEdgeId = payload.edgeId;
this.__emitChange();
break;
case ActionTypes.ENTER_NODE:
mouseOverNodeId = payload.nodeId;
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:
controlStatus = controlStatus.set(payload.nodeId, makeMap({
pending: false,
error: payload.error
}));
this.__emitChange();
break;
case ActionTypes.DO_CONTROL_SUCCESS:
controlStatus = controlStatus.set(payload.nodeId, makeMap({
pending: false,
error: null
}));
this.__emitChange();
break;
case ActionTypes.RECEIVE_CONTROL_PIPE:
controlPipes = controlPipes.set(payload.pipeId, makeOrderedMap({
id: payload.pipeId,
nodeId: payload.nodeId,
raw: payload.rawTty
}));
this.__emitChange();
break;
case ActionTypes.RECEIVE_CONTROL_PIPE_STATUS:
if (controlPipes.has(payload.pipeId)) {
controlPipes = controlPipes.setIn([payload.pipeId, 'status'], payload.status);
case ActionTypes.CLEAR_CONTROL_ERROR: {
controlStatus = controlStatus.removeIn([payload.nodeId, 'error']);
this.__emitChange();
break;
}
break;
case ActionTypes.RECEIVE_ERROR:
if (errorUrl !== null) {
errorUrl = payload.errorUrl;
case ActionTypes.CLICK_BACKGROUND: {
closeAllNodeDetails();
this.__emitChange();
break;
}
break;
case ActionTypes.RECEIVE_NODE_DETAILS:
errorUrl = null;
// disregard if node is not selected anymore
if (nodeDetails.has(payload.details.id)) {
nodeDetails = nodeDetails.update(payload.details.id, obj => {
obj.notFound = false;
obj.details = payload.details;
return obj;
});
}
this.__emitChange();
break;
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',
'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) && _.includes(mouseOverEdgeId, nodeId)) {
mouseOverEdgeId = null;
}
nodes = nodes.delete(nodeId);
});
// update existing nodes
_.each(payload.delta.update, function(node) {
if (nodes.has(node.id)) {
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, fromJS(makeNode(node)));
});
if (emitChange) {
case ActionTypes.CLICK_CLOSE_DETAILS: {
closeNodeDetails(payload.nodeId);
this.__emitChange();
break;
}
break;
case ActionTypes.RECEIVE_NOT_FOUND:
if (nodeDetails.has(payload.nodeId)) {
nodeDetails = nodeDetails.update(payload.nodeId, obj => {
obj.notFound = true;
return obj;
});
this.__emitChange();
}
break;
case ActionTypes.RECEIVE_TOPOLOGIES:
errorUrl = null;
topologyUrlsById = topologyUrlsById.clear();
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;
hostname = payload.hostname;
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;
if (payload.state.controlPipe) {
controlPipes = makeOrderedMap({
[payload.state.controlPipe.id]:
makeOrderedMap(payload.state.controlPipe)
});
} else {
case ActionTypes.CLICK_CLOSE_TERMINAL: {
controlPipes = controlPipes.clear();
this.__emitChange();
break;
}
if (payload.state.nodeDetails) {
nodeDetails = makeOrderedMap(payload.state.nodeDetails.map(obj => [obj.id, obj]));
} else {
nodeDetails = nodeDetails.clear();
case ActionTypes.CLICK_FORCE_RELAYOUT: {
forceRelayout = true;
// fire only once, reset after emitChange
setTimeout(() => {
forceRelayout = false;
}, 0);
this.__emitChange();
break;
}
topologyOptions = fromJS(payload.state.topologyOptions)
|| topologyOptions;
this.__emitChange();
break;
case ActionTypes.CLICK_NODE: {
const prevSelectedNodeId = selectedNodeId;
const prevDetailsStackSize = nodeDetails.size;
// click on sibling closes all
closeAllNodeDetails();
// select new node if it's not the same (in that case just delesect)
if (prevDetailsStackSize > 1 || prevSelectedNodeId !== payload.nodeId) {
// dont set origin if a node was already selected, suppresses animation
const origin = prevSelectedNodeId === null ? payload.origin : null;
nodeDetails = nodeDetails.set(
payload.nodeId,
{
id: payload.nodeId,
label: payload.label,
origin,
topologyId: currentTopologyId
}
);
selectedNodeId = payload.nodeId;
}
this.__emitChange();
break;
}
case ActionTypes.CLICK_PAUSE_UPDATE: {
updatePausedAt = new Date;
this.__emitChange();
break;
}
case ActionTypes.CLICK_RELATIVE: {
if (nodeDetails.has(payload.nodeId)) {
// bring to front
const details = nodeDetails.get(payload.nodeId);
nodeDetails = nodeDetails.delete(payload.nodeId);
nodeDetails = nodeDetails.set(payload.nodeId, details);
} else {
nodeDetails = nodeDetails.set(
payload.nodeId,
{
id: payload.nodeId,
label: payload.label,
origin: payload.origin,
topologyId: payload.topologyId
}
);
}
this.__emitChange();
break;
}
case ActionTypes.CLICK_RESUME_UPDATE: {
resumeUpdate();
this.__emitChange();
break;
}
case ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE: {
resumeUpdate();
nodeDetails = nodeDetails.filter((v, k) => k === payload.nodeId);
controlPipes = controlPipes.clear();
selectedNodeId = payload.nodeId;
if (payload.topologyId !== currentTopologyId) {
setTopology(payload.topologyId);
nodes = nodes.clear();
}
this.__emitChange();
break;
}
case ActionTypes.CLICK_TOPOLOGY: {
resumeUpdate();
closeAllNodeDetails();
if (payload.topologyId !== currentTopologyId) {
setTopology(payload.topologyId);
nodes = nodes.clear();
}
this.__emitChange();
break;
}
case ActionTypes.CLOSE_WEBSOCKET: {
if (!websocketClosed) {
websocketClosed = true;
this.__emitChange();
}
break;
}
case ActionTypes.DESELECT_NODE: {
closeNodeDetails();
this.__emitChange();
break;
}
case ActionTypes.DO_CONTROL: {
controlStatus = controlStatus.set(payload.nodeId, makeMap({
pending: true,
error: null
}));
this.__emitChange();
break;
}
case ActionTypes.ENTER_EDGE: {
mouseOverEdgeId = payload.edgeId;
this.__emitChange();
break;
}
case ActionTypes.ENTER_NODE: {
mouseOverNodeId = payload.nodeId;
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;
default:
break;
this.__emitChange();
break;
}
case ActionTypes.DO_CONTROL_ERROR: {
controlStatus = controlStatus.set(payload.nodeId, makeMap({
pending: false,
error: payload.error
}));
this.__emitChange();
break;
}
case ActionTypes.DO_CONTROL_SUCCESS: {
controlStatus = controlStatus.set(payload.nodeId, makeMap({
pending: false,
error: null
}));
this.__emitChange();
break;
}
case ActionTypes.RECEIVE_CONTROL_PIPE: {
controlPipes = controlPipes.set(payload.pipeId, makeOrderedMap({
id: payload.pipeId,
nodeId: payload.nodeId,
raw: payload.rawTty
}));
this.__emitChange();
break;
}
case ActionTypes.RECEIVE_CONTROL_PIPE_STATUS: {
if (controlPipes.has(payload.pipeId)) {
controlPipes = controlPipes.setIn([payload.pipeId, 'status'], payload.status);
this.__emitChange();
}
break;
}
case ActionTypes.RECEIVE_ERROR: {
if (errorUrl !== null) {
errorUrl = payload.errorUrl;
this.__emitChange();
}
break;
}
case ActionTypes.RECEIVE_NODE_DETAILS: {
errorUrl = null;
// disregard if node is not selected anymore
if (nodeDetails.has(payload.details.id)) {
nodeDetails = nodeDetails.update(payload.details.id, obj => {
const result = Object.assign({}, obj);
result.notFound = false;
result.details = payload.details;
return result;
});
}
this.__emitChange();
break;
}
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',
'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, (nodeId) => {
// in case node disappears before mouseleave event
if (mouseOverNodeId === nodeId) {
mouseOverNodeId = null;
}
if (nodes.has(nodeId) && _.includes(mouseOverEdgeId, nodeId)) {
mouseOverEdgeId = null;
}
nodes = nodes.delete(nodeId);
});
// update existing nodes
_.each(payload.delta.update, (node) => {
if (nodes.has(node.id)) {
nodes = nodes.set(node.id, nodes.get(node.id).merge(makeNode(node)));
}
});
// add new nodes
_.each(payload.delta.add, (node) => {
nodes = nodes.set(node.id, fromJS(makeNode(node)));
});
if (emitChange) {
this.__emitChange();
}
break;
}
case ActionTypes.RECEIVE_NOT_FOUND: {
if (nodeDetails.has(payload.nodeId)) {
nodeDetails = nodeDetails.update(payload.nodeId, obj => {
const result = Object.assign({}, obj);
result.notFound = true;
return result;
});
this.__emitChange();
}
break;
}
case ActionTypes.RECEIVE_TOPOLOGIES: {
errorUrl = null;
topologyUrlsById = topologyUrlsById.clear();
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;
hostname = payload.hostname;
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;
if (payload.state.controlPipe) {
controlPipes = makeOrderedMap({
[payload.state.controlPipe.id]:
makeOrderedMap(payload.state.controlPipe)
});
} else {
controlPipes = controlPipes.clear();
}
if (payload.state.nodeDetails) {
nodeDetails = makeOrderedMap(payload.state.nodeDetails.map(obj => [obj.id, obj]));
} else {
nodeDetails = nodeDetails.clear();
}
topologyOptions = fromJS(payload.state.topologyOptions)
|| topologyOptions;
this.__emitChange();
break;
}
default: {
break;
}
}
}
}

View File

@@ -5,4 +5,4 @@ import ReactDOM from 'react-dom';
import { TerminalApp } from './components/terminal-app.js';
ReactDOM.render(<TerminalApp/>, document.getElementById('app'));
ReactDOM.render(<TerminalApp />, document.getElementById('app'));

View File

@@ -1,12 +1,12 @@
jest.dontMock('../string-utils');
describe('StringUtils', function() {
describe('StringUtils', () => {
const StringUtils = require('../string-utils');
describe('formatMetric', function() {
describe('formatMetric', () => {
const formatMetric = StringUtils.formatMetric;
it('it should render 0', function() {
it('it should render 0', () => {
expect(formatMetric(0)).toBe('0.00');
});
});

View File

@@ -1,24 +1,24 @@
jest.dontMock('../web-api-utils');
describe('WebApiUtils', function() {
describe('WebApiUtils', () => {
const WebApiUtils = require('../web-api-utils');
describe('basePath', function() {
describe('basePath', () => {
const basePath = WebApiUtils.basePath;
it('should handle /scope/terminal.html', function() {
it('should handle /scope/terminal.html', () => {
expect(basePath('/scope/terminal.html')).toBe('/scope');
});
it('should handle /scope/', function() {
it('should handle /scope/', () => {
expect(basePath('/scope/')).toBe('/scope');
});
it('should handle /scope', function() {
it('should handle /scope', () => {
expect(basePath('/scope')).toBe('/scope');
});
it('should handle /', function() {
it('should handle /', () => {
expect(basePath('/')).toBe('');
});
});

View File

@@ -15,9 +15,8 @@ const letterRange = endLetterRange - startLetterRange;
function text2degree(text) {
const input = text.substr(0, 2).toUpperCase();
let num = 0;
let charCode;
for (let i = 0; i < input.length; i++) {
charCode = Math.max(Math.min(input[i].charCodeAt(), endLetterRange), startLetterRange);
const charCode = Math.max(Math.min(input[i].charCodeAt(), endLetterRange), startLetterRange);
num += Math.pow(letterRange, input.length - i - 1) * (charCode - startLetterRange);
}
hueScale.domain([0, Math.pow(letterRange, input.length)]);

View File

@@ -8,21 +8,20 @@ const prefix = {
svg: 'http://www.w3.org/2000/svg'
};
const cssSkipValues = {
'auto': true,
auto: true,
'0px 0px': true,
'visible': true,
'pointer': true
visible: true,
pointer: true
};
function setInlineStyles(svg, target, emptySvgDeclarationComputed) {
function explicitlySetStyle(element, targetEl) {
const cSSStyleDeclarationComputed = getComputedStyle(element);
let value;
let computedStyleStr = '';
_.each(cSSStyleDeclarationComputed, key => {
value = cSSStyleDeclarationComputed.getPropertyValue(key);
const value = cSSStyleDeclarationComputed.getPropertyValue(key);
if (value !== emptySvgDeclarationComputed.getPropertyValue(key) && !cssSkipValues[value]) {
computedStyleStr += key + ':' + value + ';';
computedStyleStr += `${key}:${value};`;
}
});
targetEl.setAttribute('style', computedStyleStr);
@@ -70,23 +69,22 @@ function download(source, name) {
if (name) {
filename = name;
} else if (window.document.title) {
filename = window.document.title.replace(/[^a-z0-9]/gi, '-').toLowerCase()
+ '-' + (+new Date);
filename = `${window.document.title.replace(/[^a-z0-9]/gi, '-').toLowerCase()}-${(+new Date)}`;
}
const url = window.URL.createObjectURL(new Blob(source,
{'type': 'text\/xml'}
{type: 'text\/xml'}
));
const a = document.createElement('a');
document.body.appendChild(a);
a.setAttribute('class', 'svg-crowbar');
a.setAttribute('download', filename + '.svg');
a.setAttribute('download', `${filename}.svg`);
a.setAttribute('href', url);
a.style.display = 'none';
a.click();
setTimeout(function() {
setTimeout(() => {
window.URL.revokeObjectURL(url);
}, 10);
}
@@ -120,7 +118,7 @@ function getSVG(doc, emptySvgDeclarationComputed) {
function cleanup() {
const crowbarElements = document.querySelectorAll('.svg-crowbar');
[].forEach.call(crowbarElements, function(el) {
[].forEach.call(crowbarElements, (el) => {
el.parentNode.removeChild(el);
});

View File

@@ -23,17 +23,17 @@ export function updateRoute() {
if (shouldReplaceState(prevState, state)) {
// Replace the top of the history rather than pushing on a new item.
page.replace('/state/' + stateUrl, state, dispatch);
page.replace(`/state/${stateUrl}`, state, dispatch);
} else {
page.show('/state/' + stateUrl, state, dispatch);
page.show(`/state/${stateUrl}`, state, dispatch);
}
}
page('/', function() {
page('/', () => {
updateRoute();
});
page('/state/:state', function(ctx) {
page('/state/:state', (ctx) => {
const state = JSON.parse(ctx.params.state);
route(state);
});

View File

@@ -10,34 +10,30 @@ export function findTopologyById(subTree, topologyId) {
if (!foundTopology && topology.has('sub_topologies')) {
foundTopology = findTopologyById(topology.get('sub_topologies'), topologyId);
}
if (foundTopology) {
return false;
}
});
return foundTopology;
}
export function updateNodeDegrees(nodes, edges) {
return nodes.map(node => {
const nodeId = node.get('id');
const degree = edges.count(edge => {
return edge.get('source') === nodeId || edge.get('target') === nodeId;
});
const degree = edges.count(edge => edge.get('source') === nodeId
|| edge.get('target') === nodeId);
return node.set('degree', degree);
});
}
/* set topology.id in place on each topology */
export function updateTopologyIds(topologies) {
topologies.forEach(topology => {
topology.id = topology.url.split('/').pop();
return topologies.map(topology => {
const result = Object.assign({}, topology);
result.id = topology.url.split('/').pop();
if (topology.sub_topologies) {
updateTopologyIds(topology.sub_topologies);
result.sub_topologies = updateTopologyIds(topology.sub_topologies);
}
return result;
});
return topologies;
}
// adds ID field to topology (based on last part of URL path) and save urls in

View File

@@ -46,13 +46,14 @@ function consolidateBuffer() {
let toAdd = _.union(first.add, second.add);
let toUpdate = _.union(first.update, second.update);
let toRemove = _.union(first.remove, second.remove);
log('Consolidating delta buffer', 'add', _.size(toAdd), 'update', _.size(toUpdate), 'remove', _.size(toRemove));
log('Consolidating delta buffer', 'add', _.size(toAdd), 'update',
_.size(toUpdate), 'remove', _.size(toRemove));
// check if an added node in first was updated in second -> add second update
toAdd = _.map(toAdd, node => {
const updateNode = _.find(second.update, {'id': node.id});
const updateNode = _.find(second.update, {id: node.id});
if (updateNode) {
toUpdate = _.reject(toUpdate, {'id': node.id});
toUpdate = _.reject(toUpdate, {id: node.id});
return updateNode;
}
return node;
@@ -63,18 +64,18 @@ function consolidateBuffer() {
// check if an added node in first was removed in second -> dont add, dont remove
_.each(first.add, node => {
const removedNode = _.find(second.remove, {'id': node.id});
const removedNode = _.find(second.remove, {id: node.id});
if (removedNode) {
toAdd = _.reject(toAdd, {'id': node.id});
toRemove = _.reject(toRemove, {'id': node.id});
toAdd = _.reject(toAdd, {id: node.id});
toRemove = _.reject(toRemove, {id: node.id});
}
});
// check if an updated node in first was removed in second -> remove
_.each(first.update, node => {
const removedNode = _.find(second.remove, {'id': node.id});
const removedNode = _.find(second.remove, {id: node.id});
if (removedNode) {
toUpdate = _.reject(toUpdate, {'id': node.id});
toUpdate = _.reject(toUpdate, {id: node.id});
}
});
@@ -82,7 +83,8 @@ function consolidateBuffer() {
// remove -> add is fine for the store
// update buffer
log('Consolidated delta buffer', 'add', _.size(toAdd), 'update', _.size(toUpdate), 'remove', _.size(toRemove));
log('Consolidated delta buffer', 'add', _.size(toAdd), 'update',
_.size(toUpdate), 'remove', _.size(toRemove));
deltaBuffer.set(0, {
add: toAdd.length > 0 ? toAdd : null,
update: toUpdate.length > 0 ? toUpdate : null,

View File

@@ -23,9 +23,7 @@ let controlErrorTimer = 0;
function buildOptionsQuery(options) {
if (options) {
return options.reduce(function(query, value, param) {
return `${query}&${param}=${value}`;
}, '');
return options.reduce((query, value, param) => `${query}&${param}=${value}`, '');
}
return '';
}
@@ -52,11 +50,11 @@ export function basePathSlash(urlPath) {
// "/scope" -> "/scope/"
// "/" -> "/"
//
return basePath(urlPath) + '/';
return `${basePath(urlPath)}/`;
}
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = wsProto + '://' + location.host + basePath(location.pathname);
const wsUrl = `${wsProto}://${location.host}${basePath(location.pathname)}`;
function createWebsocket(topologyUrl, optionsQuery) {
if (socket) {
@@ -67,30 +65,29 @@ function createWebsocket(topologyUrl, optionsQuery) {
// right away
}
socket = new WebSocket(wsUrl + topologyUrl
+ '/ws?t=' + updateFrequency + '&' + optionsQuery);
socket = new WebSocket(`${wsUrl}${topologyUrl}/ws?t=${updateFrequency}&${optionsQuery}`);
socket.onopen = function() {
socket.onopen = () => {
openWebsocket();
};
socket.onclose = function() {
socket.onclose = () => {
clearTimeout(reconnectTimer);
log('Closing websocket to ' + topologyUrl, socket.readyState);
log(`Closing websocket to ${topologyUrl}`, socket.readyState);
socket = null;
closeWebsocket();
reconnectTimer = setTimeout(function() {
reconnectTimer = setTimeout(() => {
createWebsocket(topologyUrl, optionsQuery);
}, reconnectTimerInterval);
};
socket.onerror = function() {
log('Error in websocket to ' + topologyUrl);
socket.onerror = () => {
log(`Error in websocket to ${topologyUrl}`);
receiveError(currentUrl);
};
socket.onmessage = function(event) {
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
receiveNodesDelta(msg);
};
@@ -103,17 +100,17 @@ export function getTopologies(options) {
const optionsQuery = buildOptionsQuery(options);
const url = `api/topology?${optionsQuery}`;
reqwest({
url: url,
success: function(res) {
url,
success: (res) => {
receiveTopologies(res);
topologyTimer = setTimeout(function() {
topologyTimer = setTimeout(() => {
getTopologies(options);
}, TOPOLOGY_INTERVAL);
},
error: function(err) {
log('Error in topology request: ' + err.responseText);
error: (err) => {
log(`Error in topology request: ${err.responseText}`);
receiveError(url);
topologyTimer = setTimeout(function() {
topologyTimer = setTimeout(() => {
getTopologies(options);
}, TOPOLOGY_INTERVAL / 2);
}
@@ -139,15 +136,15 @@ export function getNodeDetails(topologyUrlsById, nodeMap) {
const url = [topologyUrl, '/', encodeURIComponent(obj.id)]
.join('').substr(1);
reqwest({
url: url,
success: function(res) {
url,
success: (res) => {
// make sure node is still selected
if (nodeMap.has(res.node.id)) {
receiveNodeDetails(res.node);
}
},
error: function(err) {
log('Error in node details request: ' + err.responseText);
error: (err) => {
log(`Error in node details request: ${err.responseText}`);
// dont treat missing node as error
if (err.status === 404) {
receiveNotFound(obj.id);
@@ -165,13 +162,13 @@ export function getApiDetails() {
clearTimeout(apiDetailsTimer);
const url = 'api';
reqwest({
url: url,
success: function(res) {
url,
success: (res) => {
receiveApiDetails(res);
apiDetailsTimer = setTimeout(getApiDetails, API_INTERVAL);
},
error: function(err) {
log('Error in api details request: ' + err.responseText);
error: (err) => {
log(`Error in api details request: ${err.responseText}`);
receiveError(url);
apiDetailsTimer = setTimeout(getApiDetails, API_INTERVAL / 2);
}
@@ -184,16 +181,16 @@ export function doControlRequest(nodeId, control) {
+ `${encodeURIComponent(control.nodeId)}/${control.id}`;
reqwest({
method: 'POST',
url: url,
success: function(res) {
url,
success: (res) => {
receiveControlSuccess(nodeId);
if (res && res.pipe) {
receiveControlPipe(res.pipe, nodeId, res.raw_tty, true);
}
},
error: function(err) {
error: (err) => {
receiveControlError(nodeId, err.response);
controlErrorTimer = setTimeout(function() {
controlErrorTimer = setTimeout(() => {
clearControlError(nodeId);
}, 10000);
}
@@ -204,12 +201,12 @@ export function deletePipe(pipeId) {
const url = `api/pipe/${encodeURIComponent(pipeId)}`;
reqwest({
method: 'DELETE',
url: url,
success: function() {
url,
success: () => {
log('Closed the pipe!');
},
error: function(err) {
log('Error closing pipe:' + err);
error: (err) => {
log(`Error closing pipe:${err}`);
receiveError(url);
}
});
@@ -219,11 +216,11 @@ export function getPipeStatus(pipeId) {
const url = `api/pipe/${encodeURIComponent(pipeId)}/check`;
reqwest({
method: 'GET',
url: url,
error: function(err) {
url,
error: (err) => {
log('ERROR: unexpected response:', err);
},
success: function(res) {
success: (res) => {
const status = {
200: 'PIPE_ALIVE',
204: 'PIPE_DELETED'

View File

@@ -29,36 +29,36 @@
"timely": "0.1.0"
},
"devDependencies": {
"autoprefixer": "6.0.1",
"babel-core": "6.1.4",
"babel-eslint": "4.1.5",
"babel-jest": "6.0.1",
"babel-loader": "6.1.0",
"babel-preset-es2015": "6.1.4",
"babel-preset-react": "6.1.4",
"css-loader": "0.22.0",
"eslint": "1.9.0",
"eslint-config-airbnb": "1.0.0",
"eslint-loader": "1.1.1",
"autoprefixer": "6.3.3",
"babel-core": "6.7.2",
"babel-eslint": "5.0.0",
"babel-jest": "9.0.3",
"babel-loader": "6.2.4",
"babel-preset-es2015": "6.6.0",
"babel-preset-react": "6.5.0",
"css-loader": "0.23.1",
"eslint": "2.4.0",
"eslint-config-airbnb": "6.1.0",
"eslint-loader": "1.3.0",
"eslint-plugin-jasmine": "1.6.0",
"eslint-plugin-react": "3.8.0",
"file-loader": "0.8.4",
"eslint-plugin-react": "4.2.2",
"file-loader": "0.8.5",
"http-proxy-rules": "^1.0.1",
"jest-cli": "~0.7.1",
"json-loader": "0.5.3",
"less": "~2.5.1",
"less-loader": "2.2.1",
"postcss-loader": "0.7.0",
"jest-cli": "~0.9.2",
"json-loader": "0.5.4",
"less": "~2.6.1",
"less-loader": "2.2.2",
"postcss-loader": "0.8.2",
"style-loader": "0.13.0",
"url": "0.11.0",
"url-loader": "0.5.6",
"url-loader": "0.5.7",
"webpack": "~1.12.4"
},
"optionalDependencies": {
"express": "~4.13.3",
"http-proxy": "^1.12.0",
"react-hot-loader": "~1.3.0",
"webpack-dev-server": "~1.12.1"
"webpack-dev-server": "~1.14.1"
},
"scripts": {
"build": "webpack --config webpack.production.config.js",