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..e4c32ade8 --- /dev/null +++ b/client/app/scripts/charts/node-shape-circle.js @@ -0,0 +1,22 @@ +import React from 'react'; + +export default function NodeShapeCircle({onlyHighlight, highlighted, size, color}) { + const hightlightNode = ; + + if (onlyHighlight) { + return ( + + {highlighted && hightlightNode} + + ); + } + + return ( + + {highlighted && hightlightNode} + + + + + ); +} 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..71b351b10 --- /dev/null +++ b/client/app/scripts/charts/node-shape-hex.js @@ -0,0 +1,53 @@ +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({onlyHighlight, highlighted, size, color}) { + const pathProps = (v) => { + return { + d: getPoints(size * v * 2), + transform: `rotate(90) translate(-${size * getWidth(v)}, -${size * v})` + }; + }; + + const hightlightNode = ; + + if (onlyHighlight) { + return ( + + {highlighted && hightlightNode} + + ); + } + + return ( + + {highlighted && hightlightNode} + + + + + ); +} 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..ea1c1507e --- /dev/null +++ b/client/app/scripts/charts/node-shape-square.js @@ -0,0 +1,32 @@ +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 hightlightNode = ; + + if (onlyHighlight) { + return ( + + {highlighted && hightlightNode} + + ); + } + + return ( + + {highlighted && hightlightNode} + + + + + ); +} 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..70d8ff5b9 --- /dev/null +++ b/client/app/scripts/charts/node-shape-stack.js @@ -0,0 +1,34 @@ +import React from 'react'; +import _ from 'lodash'; + +function dissoc(obj, key) { + const newObj = _.clone(obj); + delete newObj[key]; + return newObj; +} + +export default function NodeShapeStack(props) { + const propsNoHighlight = dissoc(props, 'highlighted'); + const propsOnlyHighlight = Object.assign({}, props, {onlyHighlight: true}); + + const Shape = props.shape; + const [dx, dy] = [0, 4]; + const dsx = (props.size * 2 + (dx * 2)) / (props.size * 2); + const dsy = (props.size * 2 + (dy * 2)) / (props.size * 2); + const hls = [dsx, dsy]; + + return ( + + + + + + + + + + + + + ); +} diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index f78f9f4ab..13c24b402 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -5,6 +5,35 @@ 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 = { + 'circle': NodeShapeCircle, + 'hexagon': NodeShapeHex, + 'square': NodeShapeRoundedSquare, + 'cloud': NodeShapeCloud +}; + +function getNodeShape({shape, stack}) { + const nodeShape = nodeShapes[shape]; + if (!nodeShape) { + throw new Error(`Unkown shape: ${shape}!`); + } + return stack ? stackedShape(nodeShape) : nodeShape; +} + export default class Node extends React.Component { constructor(props, context) { super(props, context); @@ -52,8 +81,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..91030f316 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -142,11 +142,15 @@ export default class NodesChart extends React.Component { 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} @@ -243,7 +247,10 @@ export default class NodesChart extends React.Component { 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 }); diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index 1d11e62fe..88853b0dc 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -7,15 +7,21 @@ const log = debug('scope:debug-panel'); import { receiveNodesDelta } from '../actions/app-actions'; import AppStore from '../stores/app-store'; +const SHAPES = ['circle', 'hexagon', 'square']; +const NODE_COUNTS = [1, 2, 3]; +const STACK_VARIANTS = [true, false]; const sample = function(collection) { return _.range(_.random(4)).map(() => _.sample(collection)); }; -const deltaAdd = function(name, adjacency = []) { +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', @@ -26,6 +32,21 @@ const deltaAdd = function(name, adjacency = []) { }; }; +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); + }); + }); + })); + + receiveNodesDelta({ + add: newNodes + }); +} + function addNodes(n) { const ns = AppStore.getNodes(); const nodeNames = ns.keySeq().toJS(); @@ -33,7 +54,11 @@ function addNodes(n) { const allNodes = _(nodeNames).concat(newNodeNames).value(); receiveNodesDelta({ - add: newNodeNames.map((name) => deltaAdd(name, sample(allNodes))) + add: newNodeNames.map((name) => deltaAdd(name, + sample(allNodes)), + _.sample(SHAPES), + _.sample(STACK_VARIANTS), + _.sample(NODE_COUNTS)) }); } @@ -65,6 +90,7 @@ export class DebugToolbar extends React.Component { + ); } diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 58c261b66..fa6b4036d 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -39,8 +39,11 @@ function makeNode(node) { id: node.id, label_major: node.label_major, label_minor: node.label_minor, + node_count: node.node_count, rank: node.rank, pseudo: node.pseudo, + stack: node.stack, + shape: node.shape, adjacency: node.adjacency }; } diff --git a/client/app/styles/main.less b/client/app/styles/main.less index bafac4127..62dffa8b9 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: @node-border-stroke-width - 1; + } + 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 { diff --git a/render/expected/expected.go b/render/expected/expected.go index b4946d7a9..b06079e7b 100644 --- a/render/expected/expected.go +++ b/render/expected/expected.go @@ -10,6 +10,12 @@ import ( // Exported for testing. var ( + circle = "circle" + square = "square" + pentagon = "pentagon" + hexagon = "hexagon" + cloud = "cloud" + uncontainedServerID = render.MakePseudoNodeID(render.UncontainedID, fixture.ServerHostName) unknownPseudoNode1ID = render.MakePseudoNodeID("10.10.10.10", fixture.ServerIP, "80") unknownPseudoNode2ID = render.MakePseudoNodeID("10.10.10.11", fixture.ServerIP, "80") @@ -18,6 +24,7 @@ var ( ID: unknownPseudoNode1ID, LabelMajor: "10.10.10.10", Pseudo: true, + Shape: circle, Node: report.MakeNode().WithAdjacent(adjacent), EdgeMetadata: report.EdgeMetadata{ EgressPacketCount: newu64(70), @@ -30,6 +37,7 @@ var ( ID: unknownPseudoNode2ID, LabelMajor: "10.10.10.11", Pseudo: true, + Shape: circle, Node: report.MakeNode().WithAdjacent(adjacent), EdgeMetadata: report.EdgeMetadata{ EgressPacketCount: newu64(50), @@ -42,6 +50,7 @@ var ( ID: render.TheInternetID, LabelMajor: render.TheInternetMajor, Pseudo: true, + Shape: cloud, Node: report.MakeNode().WithAdjacent(adjacent), EdgeMetadata: report.EdgeMetadata{ EgressPacketCount: newu64(60), @@ -61,6 +70,7 @@ var ( LabelMinor: fmt.Sprintf("%s (%s)", fixture.ClientHostID, fixture.Client1PID), Rank: fixture.Client1Name, Pseudo: false, + Shape: square, Node: report.MakeNode().WithAdjacent(ServerProcessID), EdgeMetadata: report.EdgeMetadata{ EgressPacketCount: newu64(10), @@ -73,6 +83,7 @@ var ( LabelMinor: fmt.Sprintf("%s (%s)", fixture.ClientHostID, fixture.Client2PID), Rank: fixture.Client2Name, Pseudo: false, + Shape: square, Node: report.MakeNode().WithAdjacent(ServerProcessID), EdgeMetadata: report.EdgeMetadata{ EgressPacketCount: newu64(20), @@ -85,6 +96,7 @@ var ( LabelMinor: fmt.Sprintf("%s (%s)", fixture.ServerHostID, fixture.ServerPID), Rank: fixture.ServerName, Pseudo: false, + Shape: square, Node: report.MakeNode(), EdgeMetadata: report.EdgeMetadata{ IngressPacketCount: newu64(210), @@ -97,6 +109,7 @@ var ( LabelMinor: fmt.Sprintf("%s (%s)", fixture.ServerHostID, fixture.NonContainerPID), Rank: fixture.NonContainerName, Pseudo: false, + Shape: square, Node: report.MakeNode().WithAdjacent(render.TheInternetID), EdgeMetadata: report.EdgeMetadata{}, }, @@ -116,6 +129,8 @@ var ( LabelMinor: "2 processes", Rank: fixture.Client1Name, Pseudo: false, + Shape: square, + Stack: true, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID], fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID], @@ -132,6 +147,8 @@ var ( LabelMinor: "1 process", Rank: fixture.ServerName, Pseudo: false, + Shape: square, + Stack: true, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.ServerProcessNodeID], ), @@ -147,6 +164,8 @@ var ( LabelMinor: "1 process", Rank: fixture.NonContainerName, Pseudo: false, + Shape: square, + Stack: true, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID], ), @@ -168,6 +187,7 @@ var ( LabelMinor: fixture.ClientHostName, Rank: fixture.ClientContainerImageName, Pseudo: false, + Shape: hexagon, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID], fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID], @@ -185,6 +205,7 @@ var ( LabelMinor: fixture.ServerHostName, Rank: fixture.ServerContainerImageName, Pseudo: false, + Shape: hexagon, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.ServerProcessNodeID], ), @@ -201,6 +222,8 @@ var ( LabelMinor: fixture.ServerHostName, Rank: "", Pseudo: true, + Shape: square, + Stack: true, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID], ), @@ -220,6 +243,8 @@ var ( LabelMinor: "1 container", Rank: fixture.ClientContainerImageName, Pseudo: false, + Shape: hexagon, + Stack: true, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID], fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID], @@ -237,6 +262,8 @@ var ( LabelMinor: "1 container", Rank: fixture.ServerContainerImageName, Pseudo: false, + Shape: hexagon, + Stack: true, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.ServerProcessNodeID], fixture.Report.Container.Nodes[fixture.ServerContainerNodeID], @@ -253,6 +280,8 @@ var ( LabelMinor: fixture.ServerHostName, Rank: "", Pseudo: true, + Shape: square, + Stack: true, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID], ), @@ -274,6 +303,7 @@ var ( LabelMinor: "hostname.com", // after first . Rank: "hostname.com", Pseudo: false, + Shape: circle, Children: report.MakeNodeSet( fixture.Report.Container.Nodes[fixture.ServerContainerNodeID], fixture.Report.Container.Nodes[fixture.ServerProcessNodeID], @@ -291,6 +321,7 @@ var ( LabelMinor: "hostname.com", // after first . Rank: "hostname.com", Pseudo: false, + Shape: circle, Children: report.MakeNodeSet( fixture.Report.Container.Nodes[fixture.ClientContainerNodeID], fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID], @@ -307,6 +338,7 @@ var ( ID: pseudoHostID1, LabelMajor: fixture.UnknownClient1IP, Pseudo: true, + Shape: circle, Node: report.MakeNode().WithAdjacent(ServerHostRenderedID), EdgeMetadata: report.EdgeMetadata{}, Children: report.MakeNodeSet( @@ -318,6 +350,7 @@ var ( ID: pseudoHostID2, LabelMajor: fixture.UnknownClient3IP, Pseudo: true, + Shape: circle, Node: report.MakeNode().WithAdjacent(ServerHostRenderedID), EdgeMetadata: report.EdgeMetadata{}, }, @@ -325,6 +358,7 @@ var ( ID: render.TheInternetID, LabelMajor: render.TheInternetMajor, Pseudo: true, + Shape: cloud, Node: report.MakeNode().WithAdjacent(ServerHostRenderedID), EdgeMetadata: report.EdgeMetadata{}, }, @@ -340,6 +374,7 @@ var ( LabelMinor: "1 container", Rank: "ping/pong-a", Pseudo: false, + Shape: pentagon, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID], fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID], @@ -359,6 +394,7 @@ var ( LabelMinor: "1 container", Rank: "ping/pong-b", Pseudo: false, + Shape: pentagon, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.ServerProcessNodeID], fixture.Report.Container.Nodes[fixture.ServerContainerNodeID], @@ -377,6 +413,8 @@ var ( LabelMinor: fixture.ServerHostName, Rank: "", Pseudo: true, + Shape: square, + Stack: true, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID], ), @@ -387,6 +425,7 @@ var ( ID: render.TheInternetID, LabelMajor: render.TheInternetMajor, Pseudo: true, + Shape: cloud, Node: report.MakeNode().WithAdjacent(ServerPodRenderedID), EdgeMetadata: report.EdgeMetadata{ EgressPacketCount: newu64(60), @@ -404,6 +443,8 @@ var ( LabelMinor: "2 pods", Rank: fixture.ServiceID, Pseudo: false, + Shape: pentagon, + Stack: true, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID], fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID], @@ -429,6 +470,8 @@ var ( LabelMinor: fixture.ServerHostName, Rank: "", Pseudo: true, + Shape: square, + Stack: true, Children: report.MakeNodeSet( fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID], ), @@ -439,6 +482,7 @@ var ( ID: render.TheInternetID, LabelMajor: render.TheInternetMajor, Pseudo: true, + Shape: cloud, Node: report.MakeNode().WithAdjacent(ServiceRenderedID), EdgeMetadata: report.EdgeMetadata{ EgressPacketCount: newu64(60), diff --git a/render/mapping.go b/render/mapping.go index a94f95069..be6526634 100644 --- a/render/mapping.go +++ b/render/mapping.go @@ -39,6 +39,12 @@ const ( // If the output is empty, the node shall be omitted from the rendered topology. type MapFunc func(RenderableNode, report.Networks) RenderableNodes +func theInternetNode(m RenderableNode) RenderableNode { + r := newDerivedPseudoNode(TheInternetID, TheInternetMajor, m) + r.Shape = Cloud + return r +} + // MapEndpointIdentity maps an endpoint topology node to a single endpoint // renderable node. As it is only ever run on endpoint topology nodes, we // expect that certain keys are present. @@ -64,7 +70,7 @@ func MapEndpointIdentity(m RenderableNode, local report.Networks) RenderableNode // If the dstNodeAddr is not in a network local to this report, we emit an // internet node if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) { - return RenderableNodes{TheInternetID: newDerivedPseudoNode(TheInternetID, TheInternetMajor, m)} + return RenderableNodes{TheInternetID: theInternetNode(m)} } // We are a 'client' pseudo node if the port is in the ephemeral port range. @@ -117,7 +123,9 @@ func MapProcessIdentity(m RenderableNode, _ report.Networks) RenderableNodes { rank, _ = m.Latest.Lookup(process.Name) ) - return RenderableNodes{id: NewRenderableNodeWith(id, major, minor, rank, m)} + node := NewRenderableNodeWith(id, major, minor, rank, m) + node.Shape = Square + return RenderableNodes{id: node} } // MapContainerIdentity maps a container topology node to a container @@ -138,6 +146,7 @@ func MapContainerIdentity(m RenderableNode, _ report.Networks) RenderableNodes { node := NewRenderableNodeWith(id, major, minor, rank, m) node.ControlNode = m.ID + node.Shape = Hexagon return RenderableNodes{id: node} } @@ -180,7 +189,10 @@ func MapContainerImageIdentity(m RenderableNode, _ report.Networks) RenderableNo rank = imageID ) - return RenderableNodes{id: NewRenderableNodeWith(id, major, "", rank, m)} + node := NewRenderableNodeWith(id, major, "", rank, m) + node.Shape = Hexagon + node.Stack = true + return RenderableNodes{id: node} } // MapPodIdentity maps a pod topology node to pod renderable node. As it is @@ -198,7 +210,9 @@ func MapPodIdentity(m RenderableNode, _ report.Networks) RenderableNodes { rank, _ = m.Latest.Lookup(kubernetes.PodID) ) - return RenderableNodes{id: NewRenderableNodeWith(id, major, "", rank, m)} + node := NewRenderableNodeWith(id, major, "", rank, m) + node.Shape = Pentagon + return RenderableNodes{id: node} } // MapServiceIdentity maps a service topology node to service renderable node. As it is @@ -216,7 +230,10 @@ func MapServiceIdentity(m RenderableNode, _ report.Networks) RenderableNodes { rank, _ = m.Latest.Lookup(kubernetes.ServiceID) ) - return RenderableNodes{id: NewRenderableNodeWith(id, major, "", rank, m)} + node := NewRenderableNodeWith(id, major, "", rank, m) + node.Shape = Pentagon + node.Stack = true + return RenderableNodes{id: node} } // MapAddressIdentity maps an address topology node to an address renderable @@ -242,7 +259,7 @@ func MapAddressIdentity(m RenderableNode, local report.Networks) RenderableNodes // If the addr is not in a network local to this report, we emit an // internet node if !local.Contains(net.ParseIP(addr)) { - return RenderableNodes{TheInternetID: newDerivedPseudoNode(TheInternetID, TheInternetMajor, m)} + return RenderableNodes{TheInternetID: theInternetNode(m)} } // Otherwise generate a pseudo node for every @@ -281,7 +298,9 @@ func MapHostIdentity(m RenderableNode, _ report.Networks) RenderableNodes { major = hostname } - return RenderableNodes{id: NewRenderableNodeWith(id, major, minor, rank, m)} + node := NewRenderableNodeWith(id, major, minor, rank, m) + node.Shape = Circle + return RenderableNodes{id: node} } // MapEndpoint2IP maps endpoint nodes to their IP address, for joining @@ -299,7 +318,7 @@ func MapEndpoint2IP(m RenderableNode, local report.Networks) RenderableNodes { return RenderableNodes{} } if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) { - return RenderableNodes{TheInternetID: newDerivedPseudoNode(TheInternetID, TheInternetMajor, m)} + return RenderableNodes{TheInternetID: theInternetNode(m)} } // We don't always know what port a container is listening on, and @@ -376,8 +395,9 @@ func MapIP2Container(n RenderableNode, _ report.Networks) RenderableNodes { } id := MakeContainerID(containerID) - - return RenderableNodes{id: NewDerivedNode(id, n.WithParents(report.EmptySets))} + node := NewDerivedNode(id, n.WithParents(report.EmptySets)) + node.Shape = Hexagon + return RenderableNodes{id: node} } // MapEndpoint2Process maps endpoint RenderableNodes to process @@ -402,7 +422,9 @@ func MapEndpoint2Process(n RenderableNode, _ report.Networks) RenderableNodes { } id := MakeProcessID(report.ExtractHostID(n.Node), pid) - return RenderableNodes{id: NewDerivedNode(id, n.WithParents(report.EmptySets))} + node := NewDerivedNode(id, n.WithParents(report.EmptySets)) + node.Shape = Square + return RenderableNodes{id: node} } // MapProcess2Container maps process RenderableNodes to container @@ -440,12 +462,15 @@ func MapProcess2Container(n RenderableNode, _ report.Networks) RenderableNodes { if containerID, ok := n.Node.Latest.Lookup(docker.ContainerID); ok { id = MakeContainerID(containerID) node = NewDerivedNode(id, n) + node.Shape = Hexagon } else { nCopy := n.Copy() nCopy.Node = nCopy.Node.WithID("").WithTopology("") // Wipe the ID so it cannot be rendered. id = MakePseudoNodeID(UncontainedID, hostID) node = newDerivedPseudoNode(id, UncontainedMajor, nCopy) node.LabelMinor = hostID + node.Shape = Square + node.Stack = true } node.Children = node.Children.Add(n.Node) @@ -475,6 +500,8 @@ func MapProcess2Name(n RenderableNode, _ report.Networks) RenderableNodes { node.Node.Topology = "process_name" node.Node.ID = name node.Children = node.Children.Add(n.Node) + node.Shape = Square + node.Stack = true return RenderableNodes{name: node} } @@ -529,6 +556,8 @@ func MapContainer2ContainerImage(n RenderableNode, _ report.Networks) Renderable result.Node.Topology = "container_image" result.Node.ID = report.MakeContainerImageNodeID(imageID) + result.Shape = Hexagon + result.Stack = true return RenderableNodes{id: result} } @@ -561,6 +590,8 @@ func MapPod2Service(n RenderableNode, _ report.Networks) RenderableNodes { n := NewDerivedNode(id, n.WithParents(report.EmptySets)) n.Node.Counters = n.Node.Counters.Add(podsKey, 1) n.Children = n.Children.Add(n.Node) + n.Shape = Pentagon + n.Stack = true result[id] = n } return result @@ -600,6 +631,8 @@ func MapContainerImage2Name(n RenderableNode, _ report.Networks) RenderableNodes node.LabelMajor = name node.Rank = name node.Node = n.Node.Copy() // Propagate NMD for container counting. + node.Shape = Hexagon + node.Stack = true return RenderableNodes{id: node} } @@ -624,6 +657,7 @@ func MapX2Host(n RenderableNode, _ report.Networks) RenderableNodes { id := MakeHostID(report.ExtractHostID(n.Node)) result := NewDerivedNode(id, n.WithParents(report.EmptySets)) result.Children = result.Children.Add(n.Node) + result.Shape = Circle return RenderableNodes{id: result} } @@ -669,7 +703,7 @@ func MapContainer2Pod(n RenderableNode, _ report.Networks) RenderableNodes { } result.Children = result.Children.Add(n.Node) - + result.Shape = Pentagon return RenderableNodes{id: result} } @@ -693,12 +727,11 @@ func MapContainer2Hostname(n RenderableNode, _ report.Networks) RenderableNodes // Add container id key to the counters, which will later be counted to produce the minor label result.Counters = result.Counters.Add(ContainersKey, 1) - result.Node.Topology = "container_hostname" result.Node.ID = id - result.Children = result.Children.Add(n.Node) - + result.Shape = Hexagon + result.Stack = true return RenderableNodes{id: result} } diff --git a/render/renderable_node.go b/render/renderable_node.go index e79657137..c35357cd8 100644 --- a/render/renderable_node.go +++ b/render/renderable_node.go @@ -15,11 +15,22 @@ type RenderableNode struct { Pseudo bool `json:"pseudo,omitempty"` // sort-of a placeholder node, for rendering purposes Children report.NodeSet `json:"children,omitempty"` // Nodes which have been grouped into this one ControlNode string `json:"-"` // ID of node from which to show the controls in the UI + Shape string `json:"shape"` // Shape node should be rendered as + Stack bool `json:"stack"` // Should UI render this node as a stack? report.EdgeMetadata `json:"metadata"` // Numeric sums report.Node } +// Shapes that are allowed +const ( + Circle = "circle" + Square = "square" + Pentagon = "pentagon" + Hexagon = "hexagon" + Cloud = "cloud" +) + // NewRenderableNode makes a new RenderableNode func NewRenderableNode(id string) RenderableNode { return RenderableNode{ @@ -30,6 +41,7 @@ func NewRenderableNode(id string) RenderableNode { Pseudo: false, EdgeMetadata: report.EdgeMetadata{}, Node: report.MakeNode(), + Shape: Circle, } } @@ -44,6 +56,7 @@ func NewRenderableNodeWith(id, major, minor, rank string, node RenderableNode) R Children: node.Children.Copy(), EdgeMetadata: node.EdgeMetadata.Copy(), Node: node.Node.Copy(), + Shape: Circle, } } @@ -59,6 +72,7 @@ func NewDerivedNode(id string, node RenderableNode) RenderableNode { EdgeMetadata: node.EdgeMetadata.Copy(), Node: node.Node.Copy(), ControlNode: "", // Do not propagate ControlNode when making a derived node! + Shape: Circle, } } @@ -72,6 +86,7 @@ func newDerivedPseudoNode(id, major string, node RenderableNode) RenderableNode Children: node.Children.Copy(), EdgeMetadata: node.EdgeMetadata.Copy(), Node: node.Node.Copy(), + Shape: Circle, } } @@ -113,6 +128,7 @@ func (rn RenderableNode) Merge(other RenderableNode) RenderableNode { panic(result.ID) } + result.Stack = result.Stack || rn.Stack result.Children = rn.Children.Merge(other.Children) result.EdgeMetadata = rn.EdgeMetadata.Merge(other.EdgeMetadata) result.Node = rn.Node.Merge(other.Node) @@ -132,6 +148,8 @@ func (rn RenderableNode) Copy() RenderableNode { EdgeMetadata: rn.EdgeMetadata.Copy(), Node: rn.Node.Copy(), ControlNode: rn.ControlNode, + Shape: rn.Shape, + Stack: rn.Stack, } } diff --git a/render/short_lived_connections_test.go b/render/short_lived_connections_test.go index 1ac2f08fc..e3efd476b 100644 --- a/render/short_lived_connections_test.go +++ b/render/short_lived_connections_test.go @@ -74,6 +74,7 @@ var ( ID: render.TheInternetID, LabelMajor: render.TheInternetMajor, Pseudo: true, + Shape: "cloud", Node: report.MakeNode().WithAdjacent(render.MakeContainerID(containerID)), }, render.MakeContainerID(containerID): { @@ -82,6 +83,7 @@ var ( LabelMinor: serverHostID, Rank: "", Pseudo: false, + Shape: "hexagon", Node: report.MakeNode(), ControlNode: containerNodeID, }, diff --git a/tools/lint b/tools/lint index 89d534ba8..a2c405797 100755 --- a/tools/lint +++ b/tools/lint @@ -148,7 +148,7 @@ function lint_files { function list_files { if [ $# -gt 0 ]; then - find "$@" -type f | grep -vE '(^\./\.git|^\./\.pkg|/vendor/)' + find "$@" -type f | grep -vE '(^\./\.git|^\./\.pkg|/vendor/|/client/node_modules/)' else git diff --cached --name-only fi