Skip to content

Commit

Permalink
Adding Heat Resource Topology to Horizon
Browse files Browse the repository at this point in the history
Change-Id: Ie9f2040850df3d7f1fcefe68430e9103c972f80f
Implements: blueprint heat-ui-resource-topology
  • Loading branch information
Tim Schnell committed Jul 16, 2013
1 parent 3087c34 commit 8914ed9
Show file tree
Hide file tree
Showing 31 changed files with 1,376 additions and 3 deletions.
275 changes: 275 additions & 0 deletions horizon/static/horizon/js/horizon.heattop.js
@@ -0,0 +1,275 @@
/**
*
* HeatTop JS Framework
* Dependencies: jQuery 1.7.1 or later, d3 v3 or later
* Date: June 2013
* Description: JS Framework that subclasses the D3 Force Directed Graph library to create
* Heat-specific objects and relationships with the purpose of displaying
* Stacks, Resources, and related Properties in a Resource Topology Graph.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
*/

var container = "#heat_resource_topology";

if ($(container).length){
var width = $(container).width(),
height = 500,
stack_id = $("#stack_id").data("stack_id"),
ajax_url = '/project/stacks/get_d3_data/'+stack_id+'/',
graph = $("#d3_data").data("d3_data"),
force = d3.layout.force()
.nodes(graph.nodes)
.links([])
.gravity(0.1)
.charge(-2000)
.linkDistance(100)
.size([width, height])
.on("tick",tick),
svg = d3.select(container).append("svg")
.attr("width", width)
.attr("height", height),
node = svg.selectAll(".node"),
link = svg.selectAll(".link"),
needs_update = false,
nodes = force.nodes(),
links = force.links();

build_links();
update();


function update(){
node = node.data(nodes, function(d){return d.name});
link = link.data(links);

var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("node_name", function(d){ return d.name })
.attr("node_id", function(d){ return d.instance })
.call(force.drag);

nodeEnter.append("image")
.attr("xlink:href", function(d) { return d.image; })
.attr("id", function(d){return "image_"+ d.name})
.attr("x", function(d) { return d.image_x; })
.attr("y", function(d) { return d.image_y; })
.attr("width", function(d) { return d.image_size; })
.attr("height", function(d) { return d.image_size; });
node.exit().remove();

link.enter().insert("svg:line", "g.node")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
link.exit().remove();
//Setup click action for all nodes
node.on("mouseover", function(d) {
$("#info_box").html(d.info_box);
current_info = d.name;
});
node.on("mouseout", function(d) {
$("#info_box").html('');
});

force.start();
}
function tick() {
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; });

node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}

//Load initial Stack box
$("#stack_box").html(graph.stack.info_box);
//On Page load, set Action In Progress
var in_progress = false;
set_in_progress(graph.stack, node);

//If status is In Progress, start AJAX polling
var poll_time = 0;
if (in_progress == true){poll_time = 3000;}
else {poll_time = 30000;}
ajax_poll(poll_time);

function set_in_progress(stack, nodes) {
if (stack.in_progress == true){in_progress = true;}
for (var i=0;i<nodes.length;i++) {
var d = nodes[i];
if (d.in_progress == true){in_progress = true;return false;}
}
}
function findNode(name) {
for (var i=0;i<nodes.length;i++) {if (nodes[i].name === name){return nodes[i];}};
};

function findNodeIndex(name) {
for (var i=0;i<nodes.length;i++) {if (nodes[i].name==name){return i;}};
};
function addNode (node) {
nodes.push(node);
needs_update = true;
};
function removeNode (name) {
var i = 0;
var n = findNode(name);
while (i < links.length) {
if ((links[i]['source'] == n)||(links[i]['target'] == n))
{
links.splice(i,1);
}
else i++;
}
nodes.splice(findNodeIndex(name),1);
needs_update = true;
};
function remove_nodes(old_nodes, new_nodes){
//Check for removed nodes
for (var i=0;i<old_nodes.length;i++) {
var remove_node = true;
for (var j=0;j<new_nodes.length;j++) {
if (old_nodes[i].name==new_nodes[j].name){
remove_node = false;
break;
}
}
if (remove_node==true){
removeNode(old_nodes[i].name);
}
}
}
function build_links(){
for (var i=0;i<nodes.length;i++){
build_node_links(nodes[i]);
build_reverse_links(nodes[i]);
}
}
function build_node_links(node){
for (var j=0;j<node.required_by.length;j++){
var push_link = true;
var target_idx = '';
var source_idx = findNodeIndex(node.name);
//make sure target node exists
try {
target_idx = findNodeIndex(node.required_by[j]);
} catch(err) {
console.log(err);
push_link =false;
}
//check for duplicates
for (var lidx=0;lidx<links.length;lidx++) {
if ((links[lidx]['source'] == source_idx)&&(links[lidx]['target'] == target_idx))
{
push_link=false;
break;
}
}

if (push_link==true && (source_idx && target_idx)){
links.push({
'source':source_idx,
'target':target_idx,
'value':1
});
}
}
}

function build_reverse_links(node){
for (var i=0;i<nodes.length;i++){
if(nodes[i].required_by){
for (var j=0;j<nodes[i].required_by.length;j++){
var dependency = nodes[i].required_by[j];
//if new node is required by existing node, push new link
if(node.name==dependency){
links.push({
'source':findNodeIndex(nodes[i].name),
'target':findNodeIndex(node.name),
'value':1
})
}
}
}
}
}

function ajax_poll(poll_time){
setTimeout(function() {
$.getJSON(ajax_url, function(json) {
//update d3 data element
$("#d3_data").attr("data-d3_data", JSON.stringify(json));

//update stack
$("#stack_box").html(json.stack.info_box);
set_in_progress(json.stack, json.nodes);
needs_update = false;

//Check Remove nodes
remove_nodes(nodes, json.nodes);

//Check for updates and new nodes
json.nodes.forEach(function(d){
current_node = findNode(d.name);
//Check if node already exists
if (current_node) {
//Node already exists, just update it
current_node.status = d.status;

//Status has changed, image should be updated
if (current_node.image != d.image){
current_node.image = d.image;
var this_image = d3.select("#image_"+current_node.name);
this_image
.transition()
.attr("x", function(d) { return d.image_x + 5; })
.duration(100)
.transition()
.attr("x", function(d) { return d.image_x - 5; })
.duration(100)
.transition()
.attr("x", function(d) { return d.image_x + 5; })
.duration(100)
.transition()
.attr("x", function(d) { return d.image_x - 5; })
.duration(100)
.transition()
.attr("xlink:href", d.image)
.transition()
.attr("x", function(d) { return d.image_x; })
.duration(100)
.ease("bounce")
}

//Status has changed, update info_box
current_node.info_box = d.info_box;

} else {
addNode(d);
build_links();
}
});

//if any updates needed, do update now
if (needs_update==true){
update();
}
});
//if no nodes still in progress, slow AJAX polling
if (in_progress==false){poll_time = 30000;}
else {poll_time = 3000;}
ajax_poll(poll_time);
}, poll_time);
}
}
1 change: 1 addition & 0 deletions horizon/templates/horizon/_scripts.html
Expand Up @@ -38,6 +38,7 @@
<script src='{{ STATIC_URL }}horizon/js/horizon.projects.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.networktopology.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.d3piechart.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.heattop.js' type='text/javascript' charset='utf-8'></script>
{% block custom_js_files %}{% endblock %}
{% endcompress %}

Expand Down
84 changes: 84 additions & 0 deletions openstack_dashboard/dashboards/project/stacks/api.py
@@ -0,0 +1,84 @@
import json
import logging

from django.http import HttpResponse

from openstack_dashboard.api.heat import resources_list
from openstack_dashboard.api.heat import stack_get

from openstack_dashboard.dashboards.project.stacks.mappings \
import get_resource_image
from openstack_dashboard.dashboards.project.stacks.mappings \
import get_resource_status
from openstack_dashboard.dashboards.project.stacks.sro import resource_info
from openstack_dashboard.dashboards.project.stacks.sro import stack_info


LOG = logging.getLogger(__name__)


class Stack(object):
pass


def d3_data(request, stack_id=''):
try:
stack = stack_get(request, stack_id)
except:
stack = Stack()
stack.id = stack_id
stack.stack_name = request.session.get('stack_name', '')
stack.stack_status = 'DELETE_COMPLETE'
stack.stack_status_reason = 'DELETE_COMPLETE'

try:
resources = resources_list(request, stack.stack_name)
except:
resources = []

d3_data = {"nodes": [], "stack": {}}
if stack:
stack_image = get_resource_image(stack.stack_status, 'stack')
stack_node = {
'stack_id': stack.id,
'name': stack.stack_name,
'status': stack.stack_status,
'image': stack_image,
'image_size': 60,
'image_x': -30,
'image_y': -30,
'text_x': 40,
'text_y': ".35em",
'in_progress': True if (get_resource_status(stack.stack_status) ==
'IN_PROGRESS') else False,
'info_box': stack_info(stack, stack_image)
}
d3_data['stack'] = stack_node

if resources:
for resource in resources:
resource_image = get_resource_image(resource.resource_status,
resource.resource_type)
in_progress = True if (
get_resource_status(resource.resource_status)
== 'IN_PROGRESS') else False
resource_node = {
'name': resource.logical_resource_id,
'status': resource.resource_status,
'image': resource_image,
'required_by': resource.required_by,
'image_size': 50,
'image_x': -25,
'image_y': -25,
'text_x': 35,
'text_y': ".35em",
'in_progress': in_progress,
'info_box': resource_info(resource)
}
d3_data['nodes'].append(resource_node)
return json.dumps(d3_data)


def get_d3_data(request, stack_id=''):
return HttpResponse(d3_data(request, stack_id=stack_id),
content_type="application/json")

0 comments on commit 8914ed9

Please sign in to comment.