Tailor Topology Tab view to the new object model

Also refactor collecting topology data a bit and fix ugly-looking
links between topology nodes and too long `type` attr pop-up.

Any entity of object model which has dictionary under system key '?'
with keys 'type' and 'id' produces a single node. If entity doesn't
have an explicit name, it will be generated from that entity's key
(plus index, if it is a list's element).

Change-Id: I4b241aabdda906e256135d412200ff8868a77dff
Closes-Bug: #1304897
This commit is contained in:
Timur Sufiev 2014-04-10 13:07:54 +04:00
parent aed345db4a
commit 568dad4193
7 changed files with 240 additions and 185 deletions

View File

@ -13,7 +13,6 @@
# under the License.
import logging
import json
from django.conf import settings
from horizon.exceptions import ServiceCatalogException
@ -23,7 +22,7 @@ from muranodashboard.dynamic_ui.services import get_service_name
from muranoclient.common.exceptions import HTTPForbidden, HTTPNotFound
from consts import STATUS_ID_READY, STATUS_ID_NEW
from .network import get_network_params
from muranodashboard.environments import format
from muranodashboard.environments import topology
from muranodashboard.common import utils
@ -297,7 +296,7 @@ def service_get(request, environment_id, service_id):
services = services_list(request, environment_id)
LOG.debug("Return service detail for a specified id")
for service in services:
if service['?']['id'] == service_id:
if service.id == service_id:
return service
@ -347,64 +346,4 @@ def get_deployment_descr(request, environment_id, deployment_id):
def load_environment_data(request, environment_id):
environment = environment_get(request, environment_id)
return render_d3_data(environment, environment.services)
def render_d3_data(environment, services):
ext_net_name = None
d3_data = {"nodes": [], "environment": {}}
if environment:
environment_image = '/static/dashboard/img/stack-green.svg'
in_progress, status_message = \
format.get_environment_status_message(environment)
environment_node = format.create_empty_node()
environment_node['id'] = environment.id
environment_node['name'] = environment.name
environment_node['status'] = status_message
environment_node['image'] = environment_image
environment_node['in_progress'] = in_progress
environment_node['info_box'] = \
format.environment_info(environment, status_message)
d3_data['environment'] = environment_node
if services:
for service in services:
service_image = '/static/dashboard/img/stack-green.svg'
in_progress, status_message = \
format.get_environment_status_message(service)
required_by = None
if service.get('assignFloatingIP', False):
if ext_net_name:
required_by = ext_net_name
else:
ext_net_name = 'External_Network'
d3_data['nodes'].append(format.create_ext_network_node(
ext_net_name))
required_by = ext_net_name
service_node = format.create_empty_node()
service_node['name'] = service['name']
service_node['status'] = status_message
service_node['image'] = service_image
service_node['link_type'] = "unit"
service_node['in_progress'] = in_progress
service_node['info_box'] = format.appication_info(service,
service_image,
status_message)
if required_by:
service_node['required_by'] = [required_by]
d3_data['nodes'].append(service_node)
for unit in service['units']:
unit_image = '/static/dashboard/img/server-green.svg'
node = format.create_empty_node()
node['name'] = unit['name']
node['id'] = unit['id']
node['required_by'] = [service['name']]
node['flavor'] = service['flavor']
node['info_box'] = \
format.unit_info(service, unit, unit_image)
node['image'] = unit_image
node['link_type'] = "unit"
node['in_progress'] = in_progress
d3_data['nodes'].append(node)
return json.dumps(d3_data)
return topology.render_d3_data(environment)

View File

@ -1,86 +0,0 @@
# 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.
from django.template.loader import render_to_string
def get_environment_status_message(entity):
try:
status = entity['status']
except TypeError:
status = entity.status
in_progress = True
if status in ('pending', 'ready'):
in_progress = False
if status == 'pending':
status_message = 'Waiting for deployment'
elif status == 'ready':
status_message = 'Deployed'
elif status == 'deploying':
status_message = 'Deployment is in progress'
return in_progress, status_message
def appication_info(application, app_image, status):
context = {}
context['name'] = application['name']
context['type'] = application['type']
context['status'] = status
context['app_image'] = app_image
return render_to_string('services/_application_info.html',
context)
def unit_info(service, unit, unit_image):
context = {}
context['name'] = unit['name']
context['os'] = service['osImage']['type']
context['image'] = service['osImage']['name']
context['flavor'] = service['flavor']
context['unit_image'] = unit_image
return render_to_string('services/_unit_info.html',
context)
def environment_info(environment, status):
context = {}
context['name'] = environment.name
context['status'] = status
return render_to_string('services/_environment_info.html',
context)
def create_empty_node():
node = {
'name': '',
'status': 'ready',
'image': '',
'image_size': 60,
'required_by': [],
'image_x': -30,
'image_y': -30,
'text_x': 40,
'text_y': ".35em",
'link_type': "relation",
'in_progress': False,
'info_box': ''
}
return node
def create_ext_network_node(name):
node = create_empty_node()
node['name'] = name
node['image'] = '/static/dashboard/img/lb-green.svg'
node['link_type'] = "relation"
return node

View File

@ -116,7 +116,7 @@ class DeleteService(tables.DeleteAction):
try:
environment_id = self.table.kwargs.get('environment_id')
for service in self.table.data:
if service.id == service_id:
if service['?']['id'] == service_id:
api.service_delete(request,
environment_id,
service_id)

View File

@ -0,0 +1,201 @@
# Copyright (c) 2014 Mirantis, Inc.
#
# 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.
import json
import types
from django.template.loader import render_to_string
def _get_environment_status_message(entity):
if hasattr(entity, 'status'):
status = entity.status
else:
status = entity['?']['status']
in_progress = True
status_message = ''
if status in ('pending', 'ready'):
in_progress = False
if status == 'pending':
status_message = 'Waiting for deployment'
elif status == 'ready':
status_message = 'Deployed'
elif status == 'deploying':
status_message = 'Deployment is in progress'
return in_progress, status_message
def _truncate_type(type_str, num_of_chars):
if len(type_str) < num_of_chars:
return type_str
else:
parts = type_str.split('.')
type_str, type_len = parts[-1], len(parts[-1])
for part in reversed(parts[:-1]):
if type_len + len(part) + 1 > num_of_chars:
return '...' + type_str
else:
type_str = part + '.' + type_str
type_len += len(part) + 1
return type_str
def _application_info(application, app_image, status):
context = {'name': application['name'],
'type': _truncate_type(application['?']['type'], 45),
'status': status,
'app_image': app_image}
return render_to_string('services/_application_info.html',
context)
def _unit_info(unit, unit_image):
data = dict(unit)
data['type'] = _truncate_type(data['type'], 45)
context = {'data': data,
'unit_image': unit_image}
return render_to_string('services/_unit_info.html', context)
def _environment_info(environment, status):
context = {'name': environment.name,
'status': status}
return render_to_string('services/_environment_info.html',
context)
def _create_empty_node():
node = {
'name': '',
'status': 'ready',
'image': '',
'image_size': 60,
'required_by': [],
'image_x': -30,
'image_y': -30,
'text_x': 40,
'text_y': ".35em",
'link_type': "relation",
'in_progress': False,
'info_box': ''
}
return node
def _create_ext_network_node(name):
node = _create_empty_node()
node.update({'name': name,
'image': '/static/dashboard/img/lb-green.svg',
'link_type': 'relation'})
return node
def _split_seq_by_predicate(seq, predicate):
holds, not_holds = [], []
for elt in seq:
if predicate(elt):
holds.append(elt)
else:
not_holds.append(elt)
return holds, not_holds
def _is_atomic(elt):
key, value = elt
return not isinstance(value, (types.DictType, types.ListType))
def render_d3_data(environment):
if not environment:
return None
ext_net_name = None
d3_data = {"nodes": [], "environment": {}}
in_progress, status_message = _get_environment_status_message(environment)
environment_node = _create_empty_node()
environment_node.update({
'id': environment.id,
'name': environment.name,
'status': status_message,
'image': '/static/dashboard/img/stack-green.svg',
'in_progress': in_progress,
'info_box': _environment_info(environment, status_message)
})
d3_data['environment'] = environment_node
service_image = '/static/dashboard/img/stack-green.svg'
unit_image = '/static/dashboard/img/server-green.svg'
for service in environment.services:
in_progress, status_message = _get_environment_status_message(service)
required_by = None
if hasattr(service, 'assignFloatingIP'):
if ext_net_name:
required_by = ext_net_name
else:
ext_net_name = 'External_Network'
ext_network_node = _create_ext_network_node(ext_net_name)
d3_data['nodes'].append(ext_network_node)
required_by = ext_net_name
service_node = _create_empty_node()
service_node.update({
'name': service.get('name', ''),
'status': status_message,
'image': service_image,
'id': service['?']['id'],
'link_type': 'unit',
'in_progress': in_progress,
'info_box': _application_info(
service, service_image, status_message)
})
if required_by:
service_node['required_by'] = [required_by]
d3_data['nodes'].append(service_node)
def rec(node_data, node_key, parent_node=None):
node_type = node_data.get('?', {}).get('type') if \
isinstance(node_data, types.DictType) else None
atomics, containers = _split_seq_by_predicate(
node_data.iteritems(), _is_atomic)
if node_type and node_data is not parent_node:
node = _create_empty_node()
atomics.extend([('id', node_data['?']['id']),
('type', node_type),
('name', node_data.get('name', node_key))])
if parent_node is not None:
node['required_by'] = [parent_node['?']['id']]
node.update({
'id': node_data['?']['id'],
'info_box': _unit_info(atomics, unit_image),
'image': unit_image,
'link_type': 'unit',
'in_progress': in_progress})
d3_data['nodes'].append(node)
for key, value in containers:
if key == '?':
continue
if isinstance(value, types.DictType):
rec(value, key, node_data)
elif isinstance(value, types.ListType):
for index, val in enumerate(value):
rec(val, '{0}[{1}]'.format(key, index), node_data)
rec(service, None, service)
return json.dumps(d3_data)

View File

@ -381,9 +381,7 @@ class DeploymentDetailsView(tabs.TabbedTableView):
class JSONView(generic.View):
def get(self, request, **kwargs):
self.environment_id = kwargs['environment_id']
data = api.load_environment_data(request, self.environment_id)
return HttpResponse(data,
content_type="application/json")
@staticmethod
def get(request, **kwargs):
data = api.load_environment_data(request, kwargs['environment_id'])
return HttpResponse(data, content_type='application/json')

View File

@ -27,18 +27,18 @@ var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
function update(){
node = node.data(nodes, function(d) { return d.name; });
node = node.data(nodes, function(d) { return d.id; });
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; })
.attr("node_id", function(d) { return d.id; })
.call(force.drag);
nodeEnter.append("image")
.attr("xlink:href", function(d) { return d.image; })
.attr("id", function(d){ return "image_"+ d.name; })
.attr("id", function(d){ return "image_"+ d.id; })
.attr("x", function(d) { return d.image_x; })
.attr("y", function(d) { return d.image_y; })
.attr("width", function(d) { return d.image_size; })
@ -62,15 +62,12 @@ function update(){
}
function tick() {
link.attr("d", linkArc);
link.attr('d', drawLink).style('stroke-width', 3);
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
function linkArc(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
function drawLink(d) {
return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y;
}
@ -82,15 +79,15 @@ function set_in_progress(stack, nodes) {
}
}
function findNode(name) {
function findNode(id) {
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].name === name){ return nodes[i]; }
if (nodes[i].id === id){ return nodes[i]; }
}
}
function findNodeIndex(name) {
function findNodeIndex(id) {
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].name === name){ return i; }
if (nodes[i].id=== id){ return i; }
}
}
@ -99,9 +96,9 @@ function addNode (node) {
needs_update = true;
}
function removeNode (name) {
function removeNode (id) {
var i = 0;
var n = findNode(name);
var n = findNode(id);
while (i < links.length) {
if (links[i].source === n || links[i].target === n) {
links.splice(i, 1);
@ -109,7 +106,7 @@ function removeNode (name) {
i++;
}
}
nodes.splice(findNodeIndex(name),1);
nodes.splice(findNodeIndex(id),1);
needs_update = true;
}
@ -118,18 +115,19 @@ function remove_nodes(old_nodes, new_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){
if (old_nodes[i].id === new_nodes[j].id){
remove_node = false;
break;
}
}
if (remove_node === true){
removeNode(old_nodes[i].name);
removeNode(old_nodes[i].id);
}
}
}
function build_links(){
debugger;
for (var i=0;i<nodes.length;i++){
build_node_links(nodes[i]);
build_reverse_links(nodes[i]);
@ -140,7 +138,7 @@ 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);
var source_idx = findNodeIndex(node.id);
//make sure target node exists
try {
target_idx = findNodeIndex(node.required_by[j]);
@ -173,10 +171,10 @@ function build_reverse_links(node){
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){
if(node.id === dependency){
links.push({
'source':findNodeIndex(nodes[i].name),
'target':findNodeIndex(node.name),
'source':findNodeIndex(nodes[i].id),
'target':findNodeIndex(node.id),
'value':1,
'link_type': node.link_type
});
@ -202,7 +200,7 @@ function ajax_poll(poll_time){
//Check for updates and new nodes
json.nodes.forEach(function(d){
current_node = findNode(d.name);
current_node = findNode(d.id);
//Check if node already exists
if (current_node) {
//Node already exists, just update it
@ -211,7 +209,7 @@ function ajax_poll(poll_time){
//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);
var this_image = d3.select("#image_"+current_node.id);
this_image
.transition()
.attr("x", function(d) { return d.image_x + 5; })

View File

@ -1,5 +1,10 @@
<img src="{{ unit_image }}" width="35px" height="35px" />
<h3>Name: {{ name }}</h3>
<p>OS: {{ os }}</p>
<p>Image: {{ image }}</p>
<p>Flavor: {{ flavor }}</p>
{% if data.name %}
<h3>Name: {{ data.name }}</h3>
{% endif %}
{% for key, value in data.items %}
{% if key != "name" and not "password" in key|lower %}
<p>{{ key|title }}: {{ value }}</p>
{% endif %}
{% endfor %}