Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding Heat Resource Topology to Horizon
Change-Id: Ie9f2040850df3d7f1fcefe68430e9103c972f80f Implements: blueprint heat-ui-resource-topology
- Loading branch information
Tim Schnell
committed
Jul 16, 2013
1 parent
3087c34
commit 8914ed9
Showing
31 changed files
with
1,376 additions
and
3 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") |
Oops, something went wrong.