Created
December 27, 2013 06:26
-
-
Save ziadsawalha/8143348 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
********************************** | |
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