diff --git a/apps/_dashboard/__init__.py b/apps/_dashboard/__init__.py index fcab85dce..15feeab79 100644 --- a/apps/_dashboard/__init__.py +++ b/apps/_dashboard/__init__.py @@ -351,23 +351,58 @@ def url(*args): ] if len(args) == 1: + # for model d3 graphs + nodes = list() + links = list() + def tables(name): db = getattr(module, name) make_safe(db) - return [ - { + tablelist = list() + for t in getattr(module, name): + + # add links and nodes for the db graph + # code from web2py appadmin + fields = [] + for field in t: + f_type = field.type + if not isinstance(f_type, str): + disp = ' ' + elif f_type == 'string': + disp = field.length + elif f_type == 'id': + disp = 'PK' + elif f_type.startswith('reference') or \ + f_type.startswith('list:reference'): + disp = 'FK' + else: + disp = ' ' + fields.append(dict(name=field.name, type=field.type, disp=disp)) + + if isinstance(f_type, str) and ( + f_type.startswith('reference') or + f_type.startswith('list:reference')): + referenced_table = f_type.split()[1].split('.')[0] + + links.append(dict(source=t._tablename, target = referenced_table)) + + nodes.append(dict(name=t._tablename, type='table', fields = fields)) + # end of code for d3 graphs + + tablelist.append({ "name": t._tablename, "fields": t.fields, "link": url(name, t._tablename) + "?model=true", - } - for t in getattr(module, name) - ] + }) + return tablelist return { "databases": [ {"name": name, "tables": tables(name)} for name in databases - ] - } + ], + "links": links, + "nodes": nodes} + elif len(args) > 2 and args[1] in databases: db = getattr(module, args[1]) make_safe(db) diff --git a/apps/_dashboard/static/css/d3_graph.css b/apps/_dashboard/static/css/d3_graph.css new file mode 100644 index 000000000..6e9564e15 --- /dev/null +++ b/apps/_dashboard/static/css/d3_graph.css @@ -0,0 +1,33 @@ + .node {fill: steelblue; + stroke: #636363; + stroke-width: 1px;} + + .auth {fill: lightgrey;} + + .table {r: 10;} + + .link {stroke: #bbbbbb; + stroke-width: 2px;} + td {padding: 4px;} + + div.tooltip { + position: absolute; + text-align: left; + /* width: 140px; */ + /* height: 28px; */ + padding: 0px 5px 0px 5px; + padding-top: 0px; + font: 12px sans-serif; + background: #fff7bc; + border: solid 1px #aaa; + border-radius: 6px; + pointer-events: none;} + + h5 { font: 14px sans-serif; + background : #ec7014; + color: #ffffe5; + padding: 5px 2px 5px 2px; + margin-top: 1px;} + path { + fill: #aaaaaa;} + diff --git a/apps/_dashboard/static/js/d3_graph.js b/apps/_dashboard/static/js/d3_graph.js new file mode 100644 index 000000000..dff9a8da9 --- /dev/null +++ b/apps/_dashboard/static/js/d3_graph.js @@ -0,0 +1,207 @@ +// Some reference links: +// How to get link ids instead of index +// http://stackoverflow.com/questions/23986466/d3-force-layout-linking-nodes-by-name-instead-of-index +// embedding web2py in d3 +// http://stackoverflow.com/questions/34326343/embedding-d3-js-graph-in-a-web2py-bootstrap-page + +// nodes and links are retrieved by init.js from rest service +var links = Array(); +var nodes = Array(); + +function populateNodes(data){ + nodes.splice(0, nodes.length); + data.forEach(function(e){ + nodes.push(e); + }); +} + +function populateLinks(data){ + links.splice(0, links.length); + data.forEach(function(e){ + links.push(e); + }); +} + +function d3_graph(nodes, links) { + // code to flush the d3 object + var myvisdiv = document.getElementById("vis"); + myvisdiv.innerHTML = ""; + + // erease old and get the new tables and relations + populateNodes(nodes); + populateLinks(links); + + if (nodes.length == 0){ + myvisdiv.innerHTML = "No diagrams to draw"; + return + } + + var edges = []; + + links.forEach(function(e) { + var sourceNode = nodes.filter(function(n) { + return n.name === e.source; + })[0], + targetNode = nodes.filter(function(n) { + return n.name === e.target; + })[0]; + + edges.push({ + source: sourceNode, + target: targetNode, + value: 1}); + + }); + + edges.forEach(function(e) { + + if (!e.source["linkcount"]) e.source["linkcount"] = 0; + if (!e.target["linkcount"]) e.target["linkcount"] = 0; + + e.source["linkcount"]++; + e.target["linkcount"]++; + }); + + var width = window.innerWidth, height = window.innerHeight/3; + // var height = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight; + // var width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth; + var svg = d3.select("#vis").append("svg") + .attr("width", width) + .attr("height", height); + + // updated for d3 v4. + var simulation = d3.forceSimulation() + .force("link", d3.forceLink().id(function(d) { return d.id; })) + .force("charge", d3.forceManyBody().strength(strength)) + .force("center", d3.forceCenter(width / 2, height / 2)) + .force("collision", d3.forceCollide(35)); + + // Node charge strength. Repel strength greater for less links. + //function strength(d) { return -50/d["linkcount"] ; } + function strength(d) { return -25 ; } + + // Link distance. Distance increases with number of links at source and target + function distance(d) { return (60 + (d.source["linkcount"] * d.target["linkcount"])) ; } + + // Link strength. Strength is less for highly connected nodes (move towards target dist) + function strengthl(d) { return 5/(d.source["linkcount"] + d.target["linkcount"]) ; } + + simulation + .nodes(nodes) + .on("tick", tick); + + simulation.force("link") + .links(edges) + .distance(distance) + .strength(strengthl); + + // build the arrow. + svg.append("svg:defs").selectAll("marker") + .data(["end"]) // Different link/path types can be defined here + .enter().append("svg:marker") // This section adds in the arrows + .attr("id", String) + .attr("viewBox", "0 -5 10 10") + .attr("refX", 25) // Moves the arrow head out, allow for radius + .attr("refY", 0) // -1.5 + .attr("markerWidth", 6) + .attr("markerHeight", 6) + .attr("orient", "auto") + .append("svg:path") + .attr("d", "M0,-5L10,0L0,5"); + + var link = svg.selectAll('.link') + .data(edges) + .enter().append('line') + .attr("class", "link") + .attr("marker-end", "url(#end)"); + + var node = svg.selectAll(".node") + .data(nodes) + .enter().append("g") + .attr("class", function(d) { return "node " + d.type;}) + .attr('transform', function(d) { + return "translate(" + d.x + "," + d.y + ")"}) + .classed("auth", function(d) { return (d.name.startsWith("auth") ? true : false);}); + + node.call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)); + + // add the nodes + node.append('circle') + .attr('r', 16) + ; + + // add text + node.append("text") + .attr("x", 12) + .attr("dy", "-1.1em") + .text(function(d) {return d.name;}); + + node.on("mouseover", function(e, d) { + + var g = d3.select(this); // the node (table) + + // tooltip + var fields = d.fields; + var fieldformat = ""; + fields.forEach(function(d) { + fieldformat += ""; + }); + fieldformat += "
"+ d.name+""+ d.type+""+ d.disp+"
"; + var tiplength = d.fields.length; + + // Define 'div' for tooltips + var div = d3.select("body").append("div") // declare the tooltip div + .attr("class", "tooltip") // apply the 'tooltip' class + .style("opacity", 0) + .html('
' + d.name + '
' + fieldformat) + .style("left", 20 + (e.pageX) + "px")// or just (d.x + 50 + "px") + .style("top", tooltop(e, tiplength))// or ... + .transition() + .duration(800) + .style("opacity", 0.9); + }); + + function tooltop(e, tiplength) { + //aim to ensure tooltip is fully visible whenver possible + return (Math.max(e.pageY - 20 - (tiplength * 14),0)) + "px" + } + + node.on("mouseout", function(e, d) { + d3.select("body").select('div.tooltip').remove(); + }); + + // instead of waiting for force to end with : force.on('end', function() + // use .on("tick", instead. Here is the tick function + function tick() { + node.attr('transform', function(d) { + d.x = Math.max(30, Math.min(width - 16, d.x)); + d.y = Math.max(30, Math.min(height - 16, d.y)); + return "translate(" + d.x + "," + d.y + ")"; }); + + link.attr('x1', function(d) {return d.source.x;}) + .attr('y1', function(d) {return d.source.y;}) + .attr('x2', function(d) {return d.target.x;}) + .attr('y2', function(d) {return d.target.y;}); + }; + + function dragstarted(e, d) { + if (!e.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + }; + + function dragged(e, d) { + d.fx = e.x; + d.fy = e.y; + }; + + function dragended(e, d) { + if (!e.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + }; + +}; \ No newline at end of file diff --git a/apps/_dashboard/static/js/index.js b/apps/_dashboard/static/js/index.js index 9b0d1b515..840475c9c 100644 --- a/apps/_dashboard/static/js/index.js +++ b/apps/_dashboard/static/js/index.js @@ -204,7 +204,12 @@ let init = (app) => { app.vue.walk = []; var name = app.vue.selected_app.name; Q.get('../walk/'+name).then(r=>{app.vue.walk=r.json().payload;}); - Q.get('../rest/'+name).then(r=>{app.vue.databases=r.json().databases;}); + Q.get('../rest/'+name).then(r=>{ + var restpayload = r.json(); + app.vue.databases=restpayload.databases; + // d3 database graphs + d3_graph(restpayload.nodes, restpayload.links); + }); app.vue.selected_filename = null; } app.clear_tickets = () => { diff --git a/apps/_dashboard/templates/index.html b/apps/_dashboard/templates/index.html index b989c083d..c5dd49941 100644 --- a/apps/_dashboard/templates/index.html +++ b/apps/_dashboard/templates/index.html @@ -5,6 +5,7 @@ + @@ -120,6 +121,12 @@

+
+
+

Model graphs

+
+
+
@@ -264,5 +271,7 @@

{{modal.title}}

+ +