Skip to content

Instantly share code, notes, and snippets.

@ziadsawalha
Created December 27, 2013 06:26
Show Gist options
  • Save ziadsawalha/8143348 to your computer and use it in GitHub Desktop.
Save ziadsawalha/8143348 to your computer and use it in GitHub Desktop.
.canvas text {
font-family: verdana;
font-size: small;
}
.canvas rect.border {
stroke: gray;
stroke-width: 1;
fill: transparent;
display: inline;
overflow: scroll;
}
.tier>rect {
fill: transparent;
stroke: #666666;
stroke-width: 1;
display: inline;
}
.tier>.title {
font-weight: bold;
margin-left: 20px;
}
.service>rect {
stroke: #eeeeee;
fill: transparent;
}
.service>.title {
font-weight: normal;
}
.service_entry text {
font-weight: normal;
}
.service_LISTENING text {
font-weight: normal;
}
.resource>.title {
font-weight: bold;
}
.resource>rect {
stroke: #224488;
fill: transparent;
}
.resource {
cursor: pointer;
}
.link {
stroke: #000;
stroke-width: 1.5px;
}
.link.connection_OK {
stroke: green;
stroke-width: 2px;
}
.link.connection_ERROR {
stroke: red;
stroke-width: 2px;
}
<!DOCTYPE html>
<html>
<head>
<script src="http://code.jquery.com/jquery.min.js"></script>
<link href="http://getbootstrap.com/dist/css/bootstrap.css" rel="stylesheet" type="text/css" />
<script src="http://getbootstrap.com/dist/js/bootstrap.js"></script>
<script src="http://documentcloud.github.io/underscore/underscore-min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<meta charset=utf-8 />
<title>Topology</title>
</head>
<body>
<div id="my_drawing" style="overflow:auto"/>
<div id="infobox" style="display: none;"/>
</body>
</html>
/*
**********************************
topology.js - built as a library
**********************************
*/
var topology = (function(topology, $, _, d3, console) {
"use strict";
// Check dependencies (I made them injectable for future testing)
if (typeof topology == "undefined") {
topology = {};
} else
console.log("Reloading topology.js");
if (typeof $ == 'undefined')
console.error("jQuery not loaded. topology.js requires it");
if (typeof _ == 'undefined')
console.error("_.underscore not loaded. topology.js requires it");
if (typeof d3 == 'undefined')
console.error("d3 not loaded. topology.js requires it");
if (typeof topology == "undefined")
topology = {};
//private vars and functions (in library scope only)
var root = this;
// Check if an object is an array (http://shop.oreilly.com/product/9780596517748.do)
var is_array = function (value) {
return value &&
typeof value === 'object' &&
typeof value.length === 'number' &&
typeof value.splice === 'function' &&
!(value.propertyIsEnumerable('length'));
};
// Add an array.extend if it doesn't exist. We trust there is a good one there if it exists
if (typeof Array.prototype.extend === 'undefined') {
Array.prototype.extend = function (other_array) {
if (!is_array(other_array))
throw "Not an array";
other_array.forEach(function(v) {this.push(v);}, this);
};
}
// Capitalize first letter
function toTitleCase(str) {
return str.replace(/wS*/g, function(txt){
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
}
//public vars and functions (precede with "topology.")
/*
* Constructor: create and return a new diagram object
*
* Pass in a jQuery selector pointint to the element to draw a
* diagram under abd a config object to control aspects of the diagram
*
* Config options:
*
* height, width: default to 100
* border: true/false
*
* Example:
*
* var my_diagram = topology.diagram("#my_div", {height: 200}).draw(data)
*
*/
topology.diagram = function(selector, config) {
var self = this;
self.config = config;
self.height = config.height || 100;
self.width = config.width || 100;
self.parent = d3.selectAll(selector);
self.id = _.uniqueId("diagram_");
self.canvas = self.parent.selectAll("#" + self.uniqueId)
.data([1]).enter().append("svg")
.attr("width", self.width)
.attr("height", self.height)
.attr("id", self.id)
.attr("class", "canvas");
// Draw a topology diagram
self.draw = function(data) {
self.data = self.prepare_data(data);
self.max_nodes = _.max(_.map(data.tiers, function(tier) {return _.size(tier.resources || {});}));
// draw border
if (self.config.border === true) {
self.canvas.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", self.width).attr("height", self.height)
.attr("class", "border");
}
// draw tiers
self.tiers = self.draw_tiers(d3.entries(self.data.tiers));
self.draw_connections(self.data);
return self;
};
// Clean up, prepare, and index data for drawing
self.prepare_data = function(data) {
if (Object.getOwnPropertyNames(data || {}).indexOf('tiers') == -1)
throw "No 'tiers' key in data";
_.each(data.tiers, function(tier, key) {
// Find services running more than once on a single server
var dupes = [];
_.each(tier.resources, function(v, k) {
v.tier = key; // mark each resource with its tier
if (is_array(v.services)) {
var node_services = [];
_.each(v.services, function(svc) {
if (typeof svc.process === 'string') {
if (node_services.indexOf(svc.process) === -1) {
node_services.push(svc.process);
} else {
if (dupes.indexOf(svc.process) === -1)
dupes.push(svc.process);
}
}
});
}
});
// Get unique service[:port] list for this tier
var services = [];
_.each(tier.resources, function(v, k) {
if (is_array(v.services)) {
_.each(v.services, function(svc) {
if (typeof svc.process === 'string') {
if (dupes.indexOf(svc.process) === -1) {
services.push(svc.process);
} else {
services.push(svc.process + (svc.port ? ":" + svc.port : ""));
}
}
});
}
});
// Sort and clean up the list
tier.services = _.uniq(services).sort();
// Index each service in each resource so we can use it when rendering node services
_.each(tier.resources, function(v, k) {
if (is_array(v.services)) {
_.each(v.services, function(svc) {
if (typeof svc.process === 'string') {
var index = tier.services.indexOf(svc.process + (svc.port ? ":" + svc.port : ""));
if (index === -1)
index = tier.services.indexOf(svc.process);
svc.index = index;
}
});
}
});
});
return data;
};
// Draw tier boxes (and the nodes in the tier)
self.draw_tiers = function(tiers) {
var tier_height = parseInt((self.height / _.size(tiers)) - 5, 0);
var results = this.canvas.selectAll(".tier").data(tiers).enter()
.append("g")
.attr("id", function(d) {return "tier_" + d.key;})
.attr("class", "tier")
.attr("transform", function(d, index) {
d.x = 0;
d.y = 5 + (tier_height*index);
d.height = tier_height;
return "translate(" + [d.x, d.y] + ")";
});
results.append("rect")
.attr("x", 1)
.attr("y", 3)
.attr("rx", 5)
.attr("width", (this.width) - 2)
.attr("height", function(d) {return d.height - 6;});
results.append("text")
.attr("x", 4)
.attr("y", 16)
.attr("class", "title")
.text(function(d) {return d.key || "n/a";});
// draw nodes and services
self.services = results.call(self.draw_tier_services);
self.nodes = results.call(self.draw_tier_nodes);
return results;
};
// Draw the nodes for each a tier
self.draw_tier_nodes = function(tiers) {
var NODES_LEFT_MERGIN = 70, NODE_WIDTH = 80, NODE_BOTTOM_MARGIN = 30;
NODE_WIDTH = (self.width - NODES_LEFT_MERGIN) / self.max_nodes;
var node_height = parseInt((self.height / _.size(tiers[0])) - NODE_BOTTOM_MARGIN, 0);
var nodes = tiers.selectAll(".resource")
.data(function(tier) {
return d3.entries(tier.value.resources);
})
.enter()
.append("g")
.attr("id", function(d) {return "id_" + d.value.id || _.uniqueId();})
.attr("class", "resource")
.attr("transform", function(d, i) {
d.x = i * NODE_WIDTH + NODES_LEFT_MERGIN;
d.y = 20;
return "translate(" + [d.x, d.y] + ")";})
.on("click", function(d) {
self.on_click(this, d);
});
nodes.append("rect")
.attr("rx", "10").attr("ry", "10")
.attr("width", NODE_WIDTH - 5)
.attr("height", node_height - 4);
nodes.append("text")
.attr("y", 20)
.attr("class", "title")
.text(function(d) {return d.key || "n/a";});
nodes.append("text")
.attr("y", 35)
.text(function(d) {return d.value.type || "n/a";});
// draw service entries
var entries = nodes.selectAll(".service_entry")
.data(function(node) {
return node.value.services || [];
})
.enter()
.append("g")
.attr("class", function(d) {return "service_entry service_" + d.status;})
.attr("transform", function(d, i) {
d.x = 0;
d.y = d.index * 20 + 55;
return "translate(" + [d.x, d.y] + ")";})
.append("text")
.text(function(d) {return d.port || "n/a";});
return nodes;
};
// Draw services bars across nodes in a tier
self.draw_tier_services = function(tiers) {
var services = tiers.selectAll(".service")
.data(function(tier) {return _.map(tier.value.services || [], function(d) {return {key: d};});})
.enter()
.append("g")
.attr("class", "service")
.attr("transform", function(d, i) {
d.x = 5;
d.y = i * 20 + 60;
return "translate(" + [d.x, d.y] + ")";});
services.append("rect")
.attr("width", self.config.width - 5)
.attr("height", 20);
services.append("text")
.attr("y", 15)
.attr("class", "title")
.text(function(d) {return d.key.split(":")[0];});
return services;
};
// Draw connections between nodes
self.draw_connections = function(data) {
_.each(data.tiers, function(tier, tkey) {
_.each(tier.resources, function(resource, rkey) {
_.each(resource.connections, function(connection) {
self.draw_connection("#id_" + resource.id, "#id_" + connection.id, "connection_" + connection.status);
});
});
});
};
// Draw a connection between two nodes using their ids
self.draw_connection = function(from, to, status) {
var source = d3.select(from)[0][0].__data__;
var target = d3.select(to)[0][0].__data__;
var sourceTier = d3.select("#tier_" + source.value.tier)[0][0].__data__;
var targetTier = d3.select("#tier_" + target.value.tier)[0][0].__data__;
self.canvas.append("line")
.attr("class", "link")
.classed(status, 1)
.attr("x1", source.x + sourceTier.x + 35)
.attr("y1", source.y + 1 + sourceTier.y + sourceTier.height - 30) //NODE_BOTTOM_MARGIN
.attr("x2", target.x + targetTier.x + 35)
.attr("y2", target.y + targetTier.y);
};
self.on_click = function(element, node) {
$(".popover-marker").popover("hide");
_.each($(".popover-marker"), function(e) {e.classList.remove("popover-marker");});
var content = '<table>';
content += '<tr><td>Name:</td><td>' + node.key + '</td></tr>';
content += '<tr><td>Type:</td><td>' + node.value.type + '</td></tr>';
if ('id' in node)
content += '<tr><td>ID:</td><td>' + node.value.id + '</td></tr>';
if ('status' in node)
content += '<tr><td>Status:</td><td>' + node.value.status + '</td></tr>';
if (typeof node.value.url == "string")
content += '<tr><td>URL:</td><td><a href="' + node.value.url + '" target="_blank">' + node.value.url + '</a></td></tr>';
_.each(node.info, function(d, k) {
content += '<tr><td>' + toTitleCase(k) + '</td><td>' + d + '</td></tr>';
});
$("#infobox").html('<table>' + content + '</table>');
$(element).popover({
title: node.value.type + ': ' + node.key,
trigger: 'manual',
html : true,
content: function() {return $("#infobox").html();},
container: "body"
}).click(function(e) {
e.preventDefault() ;
}).popover('show');
element.classList.add("popover-marker");
console.log(element);
// handle clicking on the popover itself
$('.popover').off('click').on('click', function(e) {
e.stopPropagation(); // prevent event for bubbling up => will not get caught with document.onclick
});
};
return self; //diagram instance
};
return topology;
}).call(this, topology, this.$, this._, this.d3, console);
/*
**********************************
Using the topology.js library
**********************************
*/
var data = {
tiers: {
lb: {
resources: {
lb01: {
id: "lb01",
type: "load-balancer",
url: "http://rackspace.com",
connections: [
{id: "1000", status: "OK"},
{id: "876555-998646-3874", status: "ERROR"},
{id: "29847624", status: "OK"}
]
}
}
},
webhead: {
resources: {
srv01: {
id: "29847624",
type: "compute",
services: [
{
process: "apache2",
address: "127.0.0.1",
port: 8080,
protocol: "tcp",
status: "LISTENING"
}, {
process: "varnish",
address: "127.0.0.1",
port: 80,
protocol: "tcp",
status: "LISTENING"
}, {
process: "redis",
address: "127.0.0.1",
port: 6329,
protocol: "tcp",
status: "LISTENING"
}
],
connections: [
{id: "db01", status: "OK"}
]
},
srv02: {
id: "1000",
type: "compute",
services: [
{
process: "apache2",
address: "127.0.0.1",
port: 8080,
protocol: "tcp",
status: "LISTENING"
}, {
process: "varnish",
address: "127.0.0.1",
port: 80,
protocol: "tcp",
status: "LISTENING"
}
]
},
srv03: {},
srv04: {
id: "876555-998646-3874",
services: [
{
process: "apache2",
address: "127.0.0.1",
port: 80,
protocol: "tcp",
status: "LISTENING"
}
]
}
}
},
db: {
resources: {
db01: {
id: "db01",
type: "database",
services: [
{
process: "mysql",
address: "127.0.0.1",
port: 3535,
protocol: "tcp",
status: "LISTENING"
}
]
},
slave: {
type: "database",
services: [
{
process: "mysql",
address: "0.0.0.0",
port: 3535,
protocol: "tcp",
status: "LISTENING"
},
{
process: "mysql",
address: "127.0.0.1",
port: 3536,
protocol: "tcp",
status: "LISTENING"
}
]
}
}
}
}
};
var diagram = new topology.diagram("#my_drawing", {width: 360, height: 600});
var d1 = diagram.draw(data);
if (d1.id !== "diagram_1")
console.warn("Not getting back diagram from 'diagram.draw': ", d1.id);
/* single server */
var single = {
tiers: {
" ": {
resources: {
srv01: {
id: "29847624",
type: "compute",
services: [
{
process: "apache2",
address: "127.0.0.1",
port: 8080,
protocol: "tcp",
status: "LISTENING"
}, {
process: "varnish",
address: "127.0.0.1",
port: 80,
protocol: "tcp",
status: "LISTENING"
}
]
}
}
}
}
};
var diagram2 = new topology.diagram("#my_drawing", {width: 160, height: 200, border: true});
var d2 = diagram2.draw(single);
if (d2.id !== "diagram_2")
console.warn("Not getting back diagram from 'diagram.draw': ", d2.id);
/*
Enable code-folding on jsbin:
$(".CodeMirror")[0].CodeMirror.options.onGutterClick = CodeMirror.newFoldFunction(CodeMirror.braceRangeFinder)
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment