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:
parent
aed345db4a
commit
568dad4193
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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')
|
||||
|
|
|
@ -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; })
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Reference in New Issue