diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js
new file mode 100644
index 000000000..0d4f53839
--- /dev/null
+++ b/client/app/scripts/charts/node-shape-circle.js
@@ -0,0 +1,14 @@
+import React from 'react';
+
+export default function NodeShapeCircle({highlighted, size, color}) {
+ return (
+
+ {highlighted &&
+ }
+
+
+
+
+
+ );
+}
diff --git a/client/app/scripts/charts/node-shape-cloud.js b/client/app/scripts/charts/node-shape-cloud.js
new file mode 100644
index 000000000..770fc588b
--- /dev/null
+++ b/client/app/scripts/charts/node-shape-cloud.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import d3 from 'd3';
+
+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));
+}
+
+function getExtents(svgPath) {
+ const points = svgPath.split(' ').filter(s => s.length > 1).map(toPoint);
+ return [d3.extent(points, p => p[0]), d3.extent(points, p => p[1])];
+}
+
+export default function NodeShapeCloud({highlighted, size, color}) {
+ const [[minx, maxx], [miny, maxy]] = getExtents(CLOUD_PATH);
+ const width = (maxx - minx);
+ const height = (maxy - miny);
+ const cx = width / 2;
+ const cy = height / 2;
+ const pathSize = (width + height) / 2;
+ const baseScale = (size * 2) / pathSize;
+
+ const pathProps = (v) => {
+ return {
+ d: CLOUD_PATH,
+ transform: `scale(-${v * baseScale}) translate(-${cx},-${cy})`,
+ style: {strokeWidth: 1 / baseScale}
+ };
+ };
+
+ return (
+
+ {highlighted &&
+ }
+
+
+
+
+ );
+}
diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hex.js
new file mode 100644
index 000000000..5878544ed
--- /dev/null
+++ b/client/app/scripts/charts/node-shape-hex.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import d3 from 'd3';
+
+const line = d3.svg.line()
+ .interpolate('cardinal-closed')
+ .tension(0.25);
+
+function getWidth(h) {
+ return (Math.sqrt(3) / 2) * h;
+}
+
+function getPoints(h) {
+ const w = getWidth(h);
+ const points = [
+ [w * 0.5, 0],
+ [w, 0.25 * h],
+ [w, 0.75 * h],
+ [w * 0.5, h],
+ [0, 0.75 * h],
+ [0, 0.25 * h]
+ ];
+
+ return line(points);
+}
+
+
+export default function NodeShapeHex({highlighted, size, color}) {
+ const pathProps = (v) => {
+ return {
+ d: getPoints(size * v * 2),
+ transform: `rotate(90) translate(-${size * getWidth(v)}, -${size * v})`
+ };
+ };
+
+ return (
+
+ {highlighted &&
+ }
+
+
+
+
+ );
+}
diff --git a/client/app/scripts/charts/node-shape-rounded-square.js b/client/app/scripts/charts/node-shape-rounded-square.js
new file mode 100644
index 000000000..4542595b7
--- /dev/null
+++ b/client/app/scripts/charts/node-shape-rounded-square.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import NodeShapeSquare from './node-shape-square';
+
+// TODO how to express a cmp in terms of another cmp? (Rather than a sub-cmp as here).
+
+export default function NodeShapeRoundedSquare(props) {
+ return (
+
+ );
+}
diff --git a/client/app/scripts/charts/node-shape-square.js b/client/app/scripts/charts/node-shape-square.js
new file mode 100644
index 000000000..286fa176d
--- /dev/null
+++ b/client/app/scripts/charts/node-shape-square.js
@@ -0,0 +1,24 @@
+import React from 'react';
+
+export default function NodeShapeSquare({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})`
+ };
+ };
+
+ return (
+
+ {highlighted &&
+ }
+
+
+
+
+
+ );
+}
diff --git a/client/app/scripts/charts/node-shape-stack.js b/client/app/scripts/charts/node-shape-stack.js
new file mode 100644
index 000000000..031fa50cb
--- /dev/null
+++ b/client/app/scripts/charts/node-shape-stack.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import _ from 'lodash';
+
+export default function NodeShapeCircleStack(props) {
+ const propsNoHighlight = _.clone(props);
+ const Shape = props.shape;
+ delete propsNoHighlight.highlighted;
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js
index f78f9f4ab..b1c1b7940 100644
--- a/client/app/scripts/charts/node.js
+++ b/client/app/scripts/charts/node.js
@@ -5,6 +5,42 @@ import { Motion, spring } from 'react-motion';
import { clickNode, enterNode, leaveNode } from '../actions/app-actions';
import { getNodeColor } from '../utils/color-utils';
+import NodeShapeCircle from './node-shape-circle';
+import NodeShapeStack from './node-shape-stack';
+import NodeShapeRoundedSquare from './node-shape-rounded-square';
+import NodeShapeHex from './node-shape-hex';
+import NodeShapeCloud from './node-shape-cloud';
+
+function stackedShape(Shape) {
+ const factory = React.createFactory(NodeShapeStack);
+
+ return function(props) {
+ return factory(Object.assign({}, props, {shape: Shape}));
+ };
+}
+
+const nodeShapes = {
+ 'hosts': NodeShapeCircle,
+ 'containers': NodeShapeHex,
+ 'containers-by-hostname': stackedShape(NodeShapeHex),
+ 'containers-by-image': stackedShape(NodeShapeHex),
+ 'applications': NodeShapeRoundedSquare,
+ 'applications-by-name': stackedShape(NodeShapeRoundedSquare)
+};
+
+function isTheInternet(id) {
+ return id === 'theinternet';
+}
+
+function getNodeShape({id, pseudo, topologyId}) {
+ if (isTheInternet(id)) {
+ return NodeShapeCloud;
+ } else if (pseudo) {
+ return NodeShapeCircle;
+ }
+ return nodeShapes[topologyId];
+}
+
export default class Node extends React.Component {
constructor(props, context) {
super(props, context);
@@ -52,8 +88,11 @@ export default class Node extends React.Component {
if (this.props.pseudo) {
classNames.push('pseudo');
}
+
const classes = classNames.join(' ');
+ const NodeShapeType = getNodeShape(this.props);
+
return (
- {props.highlighted && }
-
-
-
+
{label}
diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js
index adb804830..e61c44fcc 100644
--- a/client/app/scripts/charts/nodes-chart.js
+++ b/client/app/scripts/charts/nodes-chart.js
@@ -142,6 +142,7 @@ export default class NodesChart extends React.Component {
blurred={node.get('blurred')}
focused={node.get('focused')}
highlighted={node.get('highlighted')}
+ topologyId={this.props.topologyId}
onClick={onNodeClick}
key={node.get('id')}
id={node.get('id')}
diff --git a/client/app/styles/main.less b/client/app/styles/main.less
index bafac4127..1391d1ab7 100644
--- a/client/app/styles/main.less
+++ b/client/app/styles/main.less
@@ -303,6 +303,10 @@ h2 {
}
}
+ g.stack g.shape .border {
+ stroke-width: 2px;
+ }
+
g.node {
cursor: pointer;
transition: opacity .5s ease-in-out;
@@ -357,28 +361,29 @@ h2 {
}
- circle.border {
- stroke-width: @node-border-stroke-width;
- fill: none;
- }
+ .shape {
+ .border {
+ stroke-width: @node-border-stroke-width;
+ fill: @background-color;
+ }
- circle.shadow {
- stroke: none;
- fill: @background-lighter-color;
- }
+ .shadow {
+ stroke: none;
+ fill: @background-lighter-color;
+ }
- circle.node {
- fill: @text-color;
- }
+ .node {
+ fill: @text-color;
+ }
- circle.highlighted {
- fill: @weave-blue;
- fill-opacity: @node-highlight-fill-opacity;
- stroke: @weave-blue;
- stroke-width: @node-highlight-stroke-width;
- stroke-opacity: @node-highlight-stroke-opacity;
+ .highlighted {
+ fill: @weave-blue;
+ fill-opacity: @node-highlight-fill-opacity;
+ stroke: @weave-blue;
+ stroke-width: @node-highlight-stroke-width;
+ stroke-opacity: @node-highlight-stroke-opacity;
+ }
}
-
}
.details {