Adding old topology to compliment new

There has been user/operator feedback that the new network topology
satisfies different needs than the old did. The two are actually
complimentary rather than mutually exclusive. This patch allows for both
topologies to be visible on separate tabs. Both views share the same
data model, but renders that data differently.

One interesting inclusion is triggering a resize event if an HTML
element with the d3-container CSS class is present. The reason for this
is that the svg content in a non-visible tab is rendered into a container
with 0 height and width. This causes the contents to all sit in the top
left corner of the container. Without the resize event which is used to
trigger a redo of the force layout. The plus side is now this d3 based
network topology handles window resize events. I am open to suggestions
so that a resize event is not necessary on the tab show event.

An additional area for improvement is the inline CSS in
_svg_element.html

Implements blueprint: restore-old-net-topology
Change-Id: Iba6e6ad07b9ff7705f62cdb0281904880df6e4ba
This commit is contained in:
David Lyle 2016-02-19 14:15:20 -07:00
parent 1554e281bc
commit dd80909edf
15 changed files with 1263 additions and 146 deletions

View File

@ -0,0 +1,619 @@
/* global Hogan */
/* Namespace for core functionality related to Network Topology. */
horizon.flat_network_topology = {
model: null,
fa_globe_glyph: '\uf0ac',
fa_globe_glyph_width: 15,
svg:'#topology_canvas',
svg_container:'#flatTopologyCanvasContainer',
network_tmpl:{
small:'#topology_template > .network_container_small',
normal:'#topology_template > .network_container_normal'
},
router_tmpl: {
small:'#topology_template > .router_small',
normal:'#topology_template > .router_normal'
},
instance_tmpl: {
small:'#topology_template > .instance_small',
normal:'#topology_template > .instance_normal'
},
balloon_tmpl : null,
balloon_device_tmpl : null,
balloon_port_tmpl : null,
network_index: {},
balloon_id:null,
reload_duration: 10000,
draw_mode:'normal',
network_height : 0,
previous_message : null,
element_properties:{
normal:{
network_width:270,
network_min_height:500,
top_margin:80,
default_height:50,
margin:20,
device_x:98.5,
device_width:90,
port_margin:16,
port_height:6,
port_width:82,
port_text_margin:{x:6,y:-4},
texts_bg_y:32,
type_y:46,
balloon_margin:{x:12,y:-12}
},
small :{
network_width:100,
network_min_height:400,
top_margin:50,
default_height:20,
margin:30,
device_x:47.5,
device_width:20,
port_margin:5,
port_height:3,
port_width:32.5,
port_text_margin:{x:0,y:0},
texts_bg_y:0,
type_y:0,
balloon_margin:{x:12,y:-30}
},
cidr_margin:5,
device_name_max_size:9,
device_name_suffix:'..'
},
init:function() {
var self = this;
$(self.svg_container).spin(horizon.conf.spinner_options.modal);
if($('#networktopology').length === 0) {
return;
}
self.color = d3.scale.category10();
self.balloon_tmpl = Hogan.compile($('#balloon_container').html());
self.balloon_device_tmpl = Hogan.compile($('#balloon_device').html());
self.balloon_port_tmpl = Hogan.compile($('#balloon_port').html());
$(document)
.on('click', 'a.closeTopologyBalloon', function(e) {
e.preventDefault();
self.delete_balloon();
})
.on('click', '.topologyBalloon', function(e) {
e.stopPropagation();
})
.on('click', 'a.vnc_window', function(e) {
e.preventDefault();
var vnc_window = window.open($(this).attr('href'), vnc_window, 'width=760,height=560');
self.delete_balloon();
})
.click(function(){
self.delete_balloon();
});
$('.toggle-view > .btn').click(function(){
self.draw_mode = $(this).data('value');
$('g.network').remove();
horizon.cookies.put('ntp_draw_mode',self.draw_mode);
self.data_convert();
});
$('#networktopology').on('change', function() {
self.load_network_info();
});
// register for message notifications
//horizon.networktopologymessager.addMessageHandler(self.handleMessage, this);
},
/*handleMessage:function(message) {
// noop
},*/
load_network_info:function(){
var self = this;
self.model = horizon.networktopologyloader.model;
self.data_convert();
},
select_draw_mode:function() {
var self = this;
var draw_mode = 'normal';
try {
draw_mode = horizon.cookies.get('ntp_draw_mode');
}
catch(e) {
// if the cookie does not exist, angular-cookie passes "undefined" to
// JSON.parse which throws an exception
}
if (draw_mode && (draw_mode === 'normal' || draw_mode === 'small')) {
self.draw_mode = draw_mode;
} else {
if (self.model.networks.length *
self.element_properties.normal.network_width > $('#topologyCanvas').width()) {
self.draw_mode = 'small';
} else {
self.draw_mode = 'normal';
}
horizon.cookies.put('ntp_draw_mode',self.draw_mode);
}
$('.toggle-view > .btn').each(function(){
var $this = $(this);
if($this.data('value') === self.draw_mode) {
$this.addClass('active');
} else {
$this.removeClass('active');
}
});
},
data_convert:function() {
var self = this;
var model = self.model;
$.each(model.networks, function(index, network) {
self.network_index[network.id] = index;
});
self.select_draw_mode();
var element_properties = self.element_properties[self.draw_mode];
self.network_height = element_properties.top_margin;
$.each([
{model:model.routers, type:'router'},
{model:model.servers, type:'instance'}
], function(index, devices) {
var type = devices.type;
var model = devices.model;
$.each(model, function(index, device) {
device.type = type;
device.ports = self.select_port(device.id);
var hasports = (device.ports.length <= 0) ? false : true;
device.parent_network = (hasports) ? self.select_main_port(device.ports).network_id : self.model.networks[0].id;
var height = element_properties.port_margin*(device.ports.length - 1);
device.height = (self.draw_mode === 'normal' && height > element_properties.default_height) ? height : element_properties.default_height;
device.pos_y = self.network_height;
device.port_height = (self.draw_mode === 'small' && height > device.height) ? 1 : element_properties.port_height;
device.port_margin = (self.draw_mode === 'small' && height > device.height) ? device.height/device.ports.length : element_properties.port_margin;
self.network_height += device.height + element_properties.margin;
});
});
$.each(model.networks, function(index, network) {
network.devices = [];
$.each([model.routers, model.servers],function(index, devices) {
$.each(devices,function(index, device) {
if(network.id === device.parent_network) {
network.devices.push(device);
}
});
});
});
self.network_height += element_properties.top_margin;
self.network_height = (self.network_height > element_properties.network_min_height) ? self.network_height : element_properties.network_min_height;
self.draw_topology();
},
draw_topology:function() {
var self = this;
$(self.svg_container).spin(false);
$(self.svg_container).removeClass('noinfo');
if (self.model.networks.length <= 0) {
$('g.network').remove();
$(self.svg_container).addClass('noinfo');
return;
}
var svg = d3.select(self.svg);
var element_properties = self.element_properties[self.draw_mode];
svg
.attr('width',self.model.networks.length*element_properties.network_width)
.attr('height',self.network_height);
var network = svg.selectAll('g.network')
.data(self.model.networks);
network.enter()
.append('g')
.attr('class','network')
.each(function(d){
this.appendChild(d3.select(self.network_tmpl[self.draw_mode]).node().cloneNode(true));
var $this = d3.select(this).select('.network-rect');
if (d.url) {
$this
.on('mouseover',function(){
$this.transition().style('fill', function() {
return d3.rgb(self.get_network_color(d.id)).brighter(0.5);
});
})
.on('mouseout',function(){
$this.transition().style('fill', function() {
return self.get_network_color(d.id);
});
})
.on('click',function(){
window.location.href = d.url;
});
} else {
$this.classed('nourl', true);
}
});
network
.attr('id',function(d) { return 'id_' + d.id; })
.attr('transform',function(d,i){
return 'translate(' + element_properties.network_width * i + ',' + 0 + ')';
})
.select('.network-rect')
.attr('height', function() { return self.network_height; })
.style('fill', function(d) { return self.get_network_color(d.id); });
network
.select('.network-name')
.attr('x', function() { return self.network_height/2; })
.text(function(d) { return d.name; });
network
.select('.network-cidr')
.attr('x', function(d) {
var padding = isExternalNetwork(d) ? self.fa_globe_glyph_width : 0;
return self.network_height - self.element_properties.cidr_margin -
padding;
})
.text(function(d) {
var cidr = $.map(d.subnets,function(n){
return n.cidr;
});
return cidr.join(', ');
});
function isExternalNetwork(d) {
return d['router:external'];
}
network
.select('.network-type')
.text(function(d) {
return isExternalNetwork(d) ? self.fa_globe_glyph : '';
})
.attr('x', function() {
return self.network_height - self.element_properties.cidr_margin;
});
$('[data-toggle="tooltip"]').tooltip({container: 'body'});
network.exit().remove();
var device = network.selectAll('g.device')
.data(function(d) { return d.devices; });
var device_enter = device.enter()
.append("g")
.attr('class','device')
.each(function(d){
var device_template = self[d.type + '_tmpl'][self.draw_mode];
this.appendChild(d3.select(device_template).node().cloneNode(true));
});
device_enter
.on('mouseenter',function(d){
var $this = $(this);
self.show_balloon(d,$this);
})
.on('click',function(){
d3.event.stopPropagation();
});
device
.attr('id',function(d) { return 'id_' + d.id; })
.attr('transform',function(d){
return 'translate(' + element_properties.device_x + ',' + d.pos_y + ')';
})
.select('.frame')
.attr('height',function(d) { return d.height; });
device
.select('.texts_bg')
.attr('y',function(d) {
return element_properties.texts_bg_y + d.height - element_properties.default_height;
});
device
.select('.type')
.attr('y',function(d) {
return element_properties.type_y + d.height - element_properties.default_height;
});
device
.select('.name')
.text(function(d) { return self.string_truncate(d.name); });
device.each(function(d) {
if (d.status === 'BUILD') {
d3.select(this).classed('loading',true);
} else if (d.task === 'deleting') {
d3.select(this).classed('loading',true);
if ('bl_' + d.id === self.balloon_id) {
self.delete_balloon();
}
} else {
d3.select(this).classed('loading',false);
if ('bl_' + d.id === self.balloon_id) {
var $this = $(this);
self.show_balloon(d,$this);
}
}
});
device.exit().each(function(d){
if ('bl_' + d.id === self.balloon_id) {
self.delete_balloon();
}
}).remove();
var port = device.select('g.ports')
.selectAll('g.port')
.data(function(d) { return d.ports; });
var port_enter = port.enter()
.append('g')
.attr('class','port')
.attr('id',function(d) { return 'id_' + d.id; });
port_enter
.append('line')
.attr('class','port_line');
port_enter
.append('text')
.attr('class','port_text');
device.select('g.ports').each(function(d){
this._portdata = {};
this._portdata.ports_length = d.ports.length;
this._portdata.parent_network = d.parent_network;
this._portdata.device_height = d.height;
this._portdata.port_height = d.port_height;
this._portdata.port_margin = d.port_margin;
this._portdata.left = 0;
this._portdata.right = 0;
$(this).mouseenter(function(e){
e.stopPropagation();
});
});
port.each(function(d){
var index_diff = self.get_network_index(this.parentNode._portdata.parent_network) -
self.get_network_index(d.network_id);
this._index_diff = index_diff = (index_diff >= 0)? ++index_diff : index_diff;
this._direction = (this._index_diff < 0)? 'right' : 'left';
this._index = this.parentNode._portdata[this._direction] ++;
});
port.attr('transform',function(){
var x = (this._direction === 'left') ? 0 : element_properties.device_width;
var ports_length = this.parentNode._portdata[this._direction];
var distance = this.parentNode._portdata.port_margin;
var y = (this.parentNode._portdata.device_height -
(ports_length -1)*distance)/2 + this._index*distance;
return 'translate(' + x + ',' + y + ')';
});
port
.select('.port_line')
.attr('stroke-width',function() {
return this.parentNode.parentNode._portdata.port_height;
})
.attr('stroke', function(d) {
return self.get_network_color(d.network_id);
})
.attr('x1',0).attr('y1',0).attr('y2',0)
.attr('x2',function() {
var parent = this.parentNode;
var width = (Math.abs(parent._index_diff) - 1)*element_properties.network_width +
element_properties.port_width;
return (parent._direction === 'left') ? -1*width : width;
});
port
.select('.port_text')
.attr('x',function() {
var parent = this.parentNode;
if (parent._direction === 'left') {
d3.select(this).classed('left',true);
return element_properties.port_text_margin.x*-1;
} else {
d3.select(this).classed('left',false);
return element_properties.port_text_margin.x;
}
})
.attr('y',function() {
return element_properties.port_text_margin.y;
})
.text(function(d) {
var ip_label = [];
$.each(d.fixed_ips, function() {
ip_label.push(this.ip_address);
});
return ip_label.join(',');
});
port.exit().remove();
},
get_network_color: function(network_id) {
return this.color(this.get_network_index(network_id));
},
get_network_index: function(network_id) {
return this.network_index[network_id];
},
select_port: function(device_id){
return $.map(this.model.ports,function(port){
if (port.device_id === device_id) {
return port;
}
});
},
select_main_port: function(ports){
var _self = this;
var main_port_index = 0;
var MAX_INT = 4294967295;
var min_port_length = MAX_INT;
$.each(ports, function(index, port){
var port_length = _self.sum_port_length(port.network_id, ports);
if(port_length < min_port_length){
min_port_length = port_length;
main_port_index = index;
}
});
return ports[main_port_index];
},
sum_port_length: function(network_id, ports){
var self = this;
var sum_port_length = 0;
var base_index = self.get_network_index(network_id);
$.each(ports, function(index, port){
sum_port_length += base_index - self.get_network_index(port.network_id);
});
return sum_port_length;
},
string_truncate: function(string) {
var self = this;
var str = string;
var max_size = self.element_properties.device_name_max_size;
var suffix = self.element_properties.device_name_suffix;
var bytes = 0;
for (var i = 0; i < str.length; i++) {
bytes += str.charCodeAt(i) <= 255 ? 1 : 2;
if (bytes > max_size) {
str = str.substr(0, i) + suffix;
break;
}
}
return str;
},
delete_device: function(type, device_id) {
var message = {id:device_id};
horizon.networktopologymessager.post_message(device_id,type,message,type,'delete',data={});
},
delete_port: function(router_id, port_id, network_id) {
var message = {id:port_id};
var data = {router_id: router_id, network_id: network_id};
horizon.networktopologymessager.post_message(port_id, 'router/' + router_id + '/', message, 'port', 'delete', data);
},
show_balloon:function(d,element) {
var self = this;
var element_properties = self.element_properties[self.draw_mode];
if (self.balloon_id) {
self.delete_balloon();
}
var balloon_tmpl = self.balloon_tmpl;
var device_tmpl = self.balloon_device_tmpl;
var port_tmpl = self.balloon_port_tmpl;
var balloon_id = 'bl_' + d.id;
var ports = [];
$.each(d.ports,function(i, port){
var object = {};
object.id = port.id;
object.router_id = port.device_id;
object.url = port.url;
object.port_status = port.status;
object.port_status_css = (port.status === "ACTIVE")? 'active' : 'down';
var ip_address = '';
try {
ip_address = port.fixed_ips[0].ip_address;
}catch(e){
ip_address = gettext('None');
}
var device_owner = '';
try {
device_owner = port.device_owner.replace('network:','');
}catch(e){
device_owner = gettext('None');
}
var network_id = '';
try {
network_id = port.network_id;
}catch(e) {
network_id = gettext('None');
}
object.network_id = network_id;
object.ip_address = ip_address;
object.device_owner = device_owner;
object.is_interface = (device_owner === 'router_interface' || device_owner === 'router_gateway');
ports.push(object);
});
var html;
var html_data = {
balloon_id:balloon_id,
id:d.id,
url:d.url,
name:d.name,
type:d.type,
delete_label: gettext("Delete"),
status:d.status,
status_class:(d.status === "ACTIVE")? 'active' : 'down',
status_label: gettext("STATUS"),
id_label: gettext("ID"),
interfaces_label: gettext("Interfaces"),
delete_interface_label: gettext("Delete Interface"),
open_console_label: gettext("Open Console"),
view_details_label: gettext("View Details")
};
if (d.type === 'router') {
html_data.delete_label = gettext("Delete Router");
html_data.view_details_label = gettext("View Router Details");
html_data.port = ports;
html_data.add_interface_url = d.url + 'addinterface';
html_data.add_interface_label = gettext("Add Interface");
html = balloon_tmpl.render(html_data,{
table1:device_tmpl,
table2:(ports.length > 0) ? port_tmpl : null
});
} else if (d.type === 'instance') {
html_data.delete_label = gettext("Terminate Instance");
html_data.view_details_label = gettext("View Instance Details");
html_data.console_id = d.id;
html_data.console = d.console;
html = balloon_tmpl.render(html_data,{
table1:device_tmpl
});
} else {
return;
}
$(self.svg_container).append(html);
var device_position = element.find('.frame');
var sidebar_width = $("#sidebar").width();
var navbar_height = $(".navbar").height();
var breadcrumb_height = $(".breadcrumb").outerHeight(true);
var pageheader_height = $(".page-header").outerHeight(true);
var launchbuttons_height = $(".launchButtons").height();
var height_offset = navbar_height + breadcrumb_height + pageheader_height + launchbuttons_height;
var device_offset = device_position.offset();
var x = Math.round(device_offset.left + element_properties.device_width + element_properties.balloon_margin.x - sidebar_width);
// 15 is magic pixel value that seems to make things line up
var y = Math.round(device_offset.top + element_properties.balloon_margin.y - height_offset + 15);
var $balloon = $('#' + balloon_id);
$balloon.css({
'left': '0px',
'top': y + 'px'
});
var balloon_width = $balloon.outerWidth();
var left_x = device_offset.left - balloon_width - element_properties.balloon_margin.x - sidebar_width;
var right_x = x + balloon_width + element_properties.balloon_margin.x + sidebar_width;
if (left_x > 0 && right_x > $(window).outerWidth()) {
x = left_x;
$balloon.addClass('leftPosition');
}
$balloon.css({
'left': x + 'px'
}).show();
$balloon.find('.delete-device').click(function(){
var $this = $(this);
$this.prop('disabled', true);
d3.select('#id_' + $this.data('device-id')).classed('loading',true);
self.delete_device($this.data('type'),$this.data('device-id'));
});
$balloon.find('.delete-port').click(function(){
var $this = $(this);
self.delete_port($this.data('router-id'),$this.data('port-id'),$this.data('network-id'));
});
self.balloon_id = balloon_id;
},
delete_balloon:function() {
var self = this;
if(self.balloon_id) {
$('#' + self.balloon_id).remove();
self.balloon_id = null;
}
}
};

View File

@ -53,7 +53,6 @@ function Server(data) {
} }
horizon.network_topology = { horizon.network_topology = {
model: null,
fa_globe_glyph: '\uf0ac', fa_globe_glyph: '\uf0ac',
fa_globe_glyph_width: 15, fa_globe_glyph_width: 15,
svg:'#topology_canvas', svg:'#topology_canvas',
@ -63,7 +62,6 @@ horizon.network_topology = {
zoom: d3.behavior.zoom(), zoom: d3.behavior.zoom(),
data_loaded: false, data_loaded: false,
svg_container:'#topologyCanvasContainer', svg_container:'#topologyCanvasContainer',
post_messages:'#topologyMessages',
balloonTmpl : null, balloonTmpl : null,
balloon_deviceTmpl : null, balloon_deviceTmpl : null,
balloon_portTmpl : null, balloon_portTmpl : null,
@ -71,10 +69,7 @@ horizon.network_topology = {
balloon_instanceTmpl : null, balloon_instanceTmpl : null,
network_index: {}, network_index: {},
balloonID:null, balloonID:null,
reload_duration: 10000,
network_height : 0, network_height : 0,
previous_message : null,
deleting_device : null,
init:function() { init:function() {
var self = this; var self = this;
@ -136,40 +131,41 @@ horizon.network_topology = {
horizon.cookies.put('are_networks_collapsed', !current); horizon.cookies.put('are_networks_collapsed', !current);
}); });
angular.element(window).on('message', function(e) {
var message = angular.element.parseJSON(e.originalEvent.data);
if (self.previous_message !== message.message) {
horizon.alert(message.type, message.message);
self.previous_message = message.message;
self.delete_post_message(message.iframe_id);
if (message.type == 'success' && self.deleting_device) {
self.remove_node_on_delete();
}
self.retrieve_network_info();
setTimeout(function() {
self.previous_message = null;
},10000);
}
});
angular.element('#topologyCanvasContainer').spin(horizon.conf.spinner_options.modal); angular.element('#topologyCanvasContainer').spin(horizon.conf.spinner_options.modal);
self.create_vis(); self.create_vis();
self.loading(); self.loading();
self.force_direction(0.05,70,-700); self.force_direction(0.05,70,-700);
if(horizon.networktopologyloader.model !== null) {
self.retrieve_network_info(true); self.retrieve_network_info(true);
}
d3.select(window).on('resize', function() {
var width = angular.element('#topologyCanvasContainer').width();
var height = angular.element('#topologyCanvasContainer').height();
self.force.size([width, height]).resume();
});
angular.element('#networktopology').on('change', function() {
self.retrieve_network_info(true);
});
// register for message notifications
horizon.networktopologymessager.addMessageHandler(this.handleMessage, this);
},
handleMessage:function(message) {
var self = this;
var deleteData = horizon.networktopologymessager.delete_data;
if (message.type == 'success') {
self.remove_node_on_delete(deleteData);
}
}, },
// Get the json data about the current deployment // Get the json data about the current deployment
retrieve_network_info: function(force_start) { retrieve_network_info: function(force_start) {
var self = this; var self = this;
if (angular.element('#networktopology').length === 0) {
return;
}
angular.element.getJSON(
angular.element('#networktopology').data('networktopology') + '?' + angular.element.now(),
function(data) {
self.data_loaded = true; self.data_loaded = true;
self.load_topology(data); self.load_topology(horizon.networktopologyloader.model);
if (force_start) { if (force_start) {
var i = 0; var i = 0;
self.force.start(); self.force.start();
@ -178,11 +174,6 @@ horizon.network_topology = {
i++; i++;
} }
} }
setTimeout(function() {
self.retrieve_network_info();
}, self.reload_duration);
}
);
}, },
// Load config from cookie // Load config from cookie
@ -222,7 +213,7 @@ horizon.network_topology = {
// Main svg // Main svg
self.outer_group = d3.select('#topologyCanvasContainer').append('svg') self.outer_group = d3.select('#topologyCanvasContainer').append('svg')
.attr('width', '100%') .attr('width', '100%')
.attr('height', angular.element(document).height() - 200 + "px") .attr('height', angular.element(document).height() - 270 + "px")
.attr('pointer-events', 'all') .attr('pointer-events', 'all')
.append('g') .append('g')
.call(self.zoom .call(self.zoom
@ -837,17 +828,14 @@ horizon.network_topology = {
}, },
delete_device: function(type, deviceId) { delete_device: function(type, deviceId) {
var self = this;
var message = {id:deviceId}; var message = {id:deviceId};
self.post_message(deviceId,type,message); horizon.networktopologymessager.post_message(deviceId,type,message,type,'delete',data={});
self.deleting_device = {type: type, deviceId: deviceId};
}, },
remove_node_on_delete: function () { remove_node_on_delete: function(deleteData) {
var self = this; var self = this;
var type = self.deleting_device.type; var deviceId = deleteData.device_id;
var deviceId = self.deleting_device.deviceId; switch (deleteData.device_type) {
switch (type) {
case 'router': case 'router':
self.removeNode(self.data.routers[deviceId]); self.removeNode(self.data.routers[deviceId]);
break; break;
@ -858,15 +846,18 @@ horizon.network_topology = {
case 'network': case 'network':
self.removeNode(self.data.networks[deviceId]); self.removeNode(self.data.networks[deviceId]);
break; break;
case 'port':
self.removePort(deviceId, deleteData.device_data);
break;
} }
self.delete_balloon(); self.delete_balloon();
}, },
delete_port: function(routerId, portId, networkId) { removePort: function(portId, deviceData) {
var self = this; var self = this;
var message = {id:portId}; var routerId = deviceData.router_id;
var networkId = deviceData.network_id;
if (routerId) { if (routerId) {
self.post_message(portId, 'router/' + routerId + '/', message);
for (var l in self.links) { for (var l in self.links) {
var data = null; var data = null;
if(self.links[l].source.data.id == routerId && self.links[l].target.data.id == networkId) { if(self.links[l].source.data.id == routerId && self.links[l].target.data.id == networkId) {
@ -874,7 +865,6 @@ horizon.network_topology = {
} else if (self.links[l].target.data.id == routerId && self.links[l].source.data.id == networkId) { } else if (self.links[l].target.data.id == routerId && self.links[l].source.data.id == networkId) {
data = self.links[l].target.data; data = self.links[l].target.data;
} }
if (data) { if (data) {
for (var p in data.ports) { for (var p in data.ports) {
if ((data.ports[p].id == portId) && (data.ports[p].network_id == networkId)) { if ((data.ports[p].id == portId) && (data.ports[p].network_id == networkId)) {
@ -888,8 +878,16 @@ horizon.network_topology = {
} }
} }
} }
}
},
delete_port: function(routerId, portId, networkId) {
var message = {id:portId};
var data = {network_id:networkId,routerId:routerId};
if (routerId) {
horizon.networktopologymessager.post_message(portId, 'router/' + routerId + '/', message, 'port', 'delete', data);
} else { } else {
self.post_message(portId, 'network/' + networkId + '/', message); horizon.networktopologymessager.post_message(portId, 'network/' + networkId + '/', message, 'port', 'delete', data);
} }
}, },
@ -1062,21 +1060,5 @@ horizon.network_topology = {
default: default:
return ''; return '';
} }
},
post_message: function(id,url,message) {
var self = this;
var iframeID = 'ifr_' + id;
var iframe = angular.element('<iframe width="500" height="300" />')
.attr('id',iframeID)
.attr('src',url)
.appendTo(self.post_messages);
iframe.on('load',function() {
angular.element(this).get(0).contentWindow.postMessage(
JSON.stringify(message, null, 2), '*');
});
},
delete_post_message: function(id) {
angular.element('#' + id).remove();
} }
}; };

View File

@ -0,0 +1,149 @@
// Aggregate for common network topology functionality
horizon.networktopologycommon = {
init:function() {
horizon.networktopologyloader.init();
horizon.networktopologymessager.init();
}
};
/**
* Common data loader for network topology views
*/
horizon.networktopologyloader = {
// data for the network topology views
model: null,
// timeout length
reload_duration: 10000,
// timer controlling update intervals
update_timer: null,
init:function() {
var self = this;
if($('#networktopology').length === 0) {
return;
}
self.update();
},
/**
* makes the data reqeuest and populates the 'model'
*/
update:function() {
var self = this;
angular.element.getJSON(
angular.element('#networktopology').data('networktopology') + '?' + angular.element.now(),
function(data) {
self.model = data;
$('#networktopology').trigger('change');
self.update_timer = setTimeout(function(){
self.update();
}, self.reload_duration);
}
);
},
/**
* stops the data update sequences
*/
stop_update:function() {
var self = this;
clearTimeout(self.update_timer);
}
};
/**
* common utility for network topology view to create iframes and pass post
* messages from those iframes
*/
horizon.networktopologymessager = {
previous_message : null,
// element to attach messages to
post_messages:'#topologyMessages',
// Array of functions to call when a message event is received
messaging_functions: [],
// data stored when a delete operation is finalizing
delete_data: {},
init:function() {
var self = this;
// listens for message events
angular.element(window).on('message', function(e) {
var message = angular.element.parseJSON(e.originalEvent.data);
if (self.previous_message !== message.message) {
horizon.alert(message.type, message.message);
self.previous_message = message.message;
self.delete_post_message(message.iframe_id);
self.messageNotify(message);
horizon.networktopologyloader.update();
setTimeout(function() {
self.previous_message = null;
self.delete_data = {};
},self.reload_duration);
}
});
},
/**
* add method to be called when a message is received
*
* @param {function} fn method to be called
*
* @param {Object} fnObj object the method is being called from this make
* sure the scope of 'this' is correct
*/
addMessageHandler:function(fn, fnObj) {
var self = this;
self.messaging_functions.push({obj:fnObj, func:fn});
},
/**
* calls the methods that subscribed to message notifications
*
* @param {Object} message iframe message content
*/
messageNotify:function(message) {
var self = this;
for (var i = 0; i < self.messaging_functions.length; i += 1) {
func = self.messaging_functions[i].func;
fnObj = self.messaging_functions[i].obj;
func.call(fnObj, message);
}
},
/**
* posts a message from the iframe
*
* @param {String} id target device id
* @param {String} url URL for action
* @param {Object} message object containing message value
* @param {String} action value of action, e.g., "delete"
* @param {Object} data of extra data that should be relayed with the message
* notification
*/
post_message: function(id,url,message,type,action,data) {
var self = this;
if(action == "delete") {
self.delete_data.device_id = id;
self.delete_data.device_type = type;
self.delete_data.device_data = data;
}
// stop the update refesh cycle while action takes place
horizon.networktopologyloader.stop_update();
var iframeID = 'ifr_' + id;
var iframe = $('<iframe width="500" height="300" />')
.attr('id',iframeID)
.attr('src',url)
.appendTo('#topologyMessages');
iframe.on('load',function() {
angular.element(this).get(0).contentWindow.postMessage(
JSON.stringify(message, null, 2), '*');
});
},
// delete the iframe
delete_post_message: function(id) {
angular.element('#' + id).remove();
}
};

View File

@ -56,6 +56,12 @@ horizon.addInitFunction(horizon.tabs.init = function () {
$content.find("table.datatable").each(function () { $content.find("table.datatable").each(function () {
horizon.datatables.update_footer_count($(this)); horizon.datatables.update_footer_count($(this));
}); });
// d3 renders incorrectly in a hidden tab, this forces a rerender when the
// container size is not 0 from display:none
if($content.find(".d3-container").length) {
window.dispatchEvent(new Event('resize'));
}
data[$tab.closest(".nav-tabs").attr("id")] = $tab.attr('data-target'); data[$tab.closest(".nav-tabs").attr("id")] = $tab.attr('data-target');
horizon.cookies.put("tabs", data); horizon.cookies.put("tabs", data);
}); });

View File

@ -0,0 +1,42 @@
# 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.utils.translation import ugettext_lazy as _
from horizon import tabs
from openstack_dashboard.dashboards.project.network_topology import utils
class TopologyBaseTab(tabs.Tab):
def get_context_data(self, request):
return utils.get_context(request)
class TopologyTab(TopologyBaseTab):
name = _("Topology")
slug = "topology"
preload = False
template_name = ("project/network_topology/_topology_view.html")
class GraphTab(TopologyBaseTab):
name = _("Graph")
slug = "graph"
preload = True
template_name = ("project/network_topology/_graph_view.html")
class TopologyTabs(tabs.TabGroup):
slug = "topology_tabs"
tabs = (TopologyTab, GraphTab)
sticky = True

View File

@ -0,0 +1,53 @@
{% load i18n %}
<div class="launchButtons pull-right">
{% if launch_instance_allowed %}
{% if show_ng_launch %}
{% url 'horizon:project:network_topology:index' as networkUrl %}
<a href="javascript:void(0);" ng-controller="LaunchInstanceModalController as modal"
ng-click="modal.openLaunchInstanceWizard({successUrl: '{{networkUrl}}'})"
id="instances__action_launch-ng" class="btn btn-default btn-launch
{% if instance_quota_exceeded %}disabled{% endif %}">
<span class="fa fa-cloud-upload"></span>
{% if instance_quota_exceeded %}
{% trans "Launch Instance (Quota exceeded)" %}
{% else %}
{% trans "Launch Instance" %}
{% endif %}
</a>
{% endif %}
{% if show_legacy_launch %}
<a href="{% url 'horizon:project:network_topology:launchinstance' %}"
id="instances__action_launch" class="btn btn-default btn-launch ajax-modal {% if instance_quota_exceeded %}disabled{% endif %}">
<span class="fa fa-cloud-upload"></span>
{% if instance_quota_exceeded %}
{% trans "Launch Instance (Quota exceeded)" %}
{% else %}
{% trans "Launch Instance" %}
{% endif %}
</a>
{% endif %}
{% endif %}
{% if create_network_allowed %}
<a href="{% url 'horizon:project:network_topology:createnetwork' %}"
id="networks__action_create" class="btn btn-default ajax-modal {% if network_quota_exceeded %}disabled{% endif %}">
<span class="fa fa-plus"></span>
{% if network_quota_exceeded %}
{% trans "Create Network (Quota exceeded)" %}
{% else %}
{% trans "Create Network" %}
{% endif %}
</a>
{% endif %}
{% if create_router_allowed %}
<a href="{% url 'horizon:project:network_topology:createrouter' %}"
id="Routers__action_create" class="btn btn-default ajax-modal {% if router_quota_exceeded %}disabled{% endif %}">
<span class="fa fa-plus"></span>
{% if router_quota_exceeded %}
{% trans "Create Router (Quota exceeded)" %}
{% else %}
{% trans "Create Router" %}
{% endif %}
</a>
{% endif %}
</div>

View File

@ -0,0 +1,21 @@
{% load i18n %}
<div class='description'>
{% blocktrans %}
Resize the canvas by scrolling up/down with your mouse/trackpad on the topology.
Pan around the canvas by clicking and dragging the space behind the topology.
{% endblocktrans %}
</div>
<div class="topology-navi">
<div class="toggle-view btn-group" data-toggle="buttons-radio">
<button type="button" class="btn btn-default" id="toggle_labels">
<span class="fa fa-th-large"></span> {% trans "Toggle Labels" %}
</button>
<button type="button" class="btn btn-default" id="toggle_networks">
<span class="fa fa-th"></span> {% trans "Toggle Network Collapse" %}
</button>
</div>
</div>
<div id="topologyCanvasContainer" class="d3-container">
<div class="nodata">{% blocktrans %}There are no networks, routers, or connected instances to display.{% endblocktrans %}</div>
</div>

View File

@ -0,0 +1,222 @@
{% load i18n %}
<style type="text/css">
svg#topology_canvas {
font-family: sans-serif;
}
svg#topology_canvas .network-rect {
cursor: pointer;
}
svg#topology_canvas .network-rect.nourl {
cursor: auto;
}
svg#topology_canvas .network-name {
font-weight: bold;
font-size: 12px;
fill: #fff;
text-anchor: middle;
}
svg#topology_canvas .network-cidr {
font-size: 11px;
text-anchor: end;
}
svg#topology_canvas text.network-type {
font-family: FontAwesome;
text-anchor: end;
}
svg#topology_canvas .port_text {
font-size: 9px;
fill: #666;
}
svg#topology_canvas .port_text.left {
text-anchor: end;
}
svg#topology_canvas .base_bg_normal {
fill: #333;
}
svg#topology_canvas .loading_bg_normal {
fill: #555;
}
svg#topology_canvas .base_bg_small,
svg#topology_canvas .loading_bg_small {
fill: #fff;
}
svg#topology_canvas .active {
fill: #45B035;
}
svg#topology_canvas .icon polygon {
fill: #333;
}
svg#topology_canvas .instance_small .frame,
svg#topology_canvas .router_small .frame {
fill: url(#device_small_bg);
stroke: #333;
stroke-width: 3;
}
svg#topology_canvas .instance_small .port_text,
svg#topology_canvas .router_small .port_text {
display: none;
}
svg#topology_canvas .router_normal .frame,
svg#topology_canvas .instance_normal .frame {
fill: #fff;
stroke: #333;
stroke-width: 4;
}
svg#topology_canvas .router_normal .icon_bg,
svg#topology_canvas .instance_normal .icon_bg {
fill: #fff;
stroke: #333;
stroke-width: 4;
}
svg#topology_canvas .router_normal .texts_bg,
svg#topology_canvas .instance_normal .texts_bg {
fill: url('#device_normal_bg');
}
svg#topology_canvas .router_normal .texts .name,
svg#topology_canvas .instance_normal .texts .name {
text-anchor: middle;
fill: #333;
font-size: 12px;
}
svg#topology_canvas .router_normal .texts .type,
svg#topology_canvas .instance_normal .texts .type {
text-anchor: middle;
fill: #fff;
font-size: 12px;
}
svg#topology_canvas .router_normal .instance_bg,
svg#topology_canvas .instance_normal .instance_bg {
fill: #333;
}
svg#topology_canvas g.loading .active {
fill: #555;
}
svg#topology_canvas g.loading .icon polygon {
fill: #555;
}
svg#topology_canvas g.loading .instance_bg {
fill: #555;
}
svg#topology_canvas g.loading .instance_small .frame,
svg#topology_canvas g.loading .router_small .frame {
stroke: #555;
fill: url(#device_small_bg_loading);
}
svg#topology_canvas g.loading .router_normal .frame,
svg#topology_canvas g.loading .instance_normal .frame {
stroke: #555;
}
svg#topology_canvas g.loading .router_normal .name,
svg#topology_canvas g.loading .instance_normal .name {
fill: #999;
}
svg#topology_canvas g.loading .router_normal .texts_bg,
svg#topology_canvas g.loading .instance_normal .texts_bg {
fill: url(#device_normal_bg_loading);
}
svg#topology_canvas g.loading .router_normal .icon_bg,
svg#topology_canvas g.loading .instance_normal .icon_bg {
stroke: #555;
}
</style>
<svg width="400" height="400" id="topology_canvas">
<defs>
<pattern id="device_normal_bg" patternUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
<g>
<rect width="20" height="20" class="base_bg_normal"></rect>
</g>
</pattern>
<pattern id="device_normal_bg_loading" patternUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
<g>
<rect width="20" height="20" class="loading_bg_normal"></rect>
<path d='M0 20L20 0ZM22 18L18 22ZM-2 2L2 -2Z' stroke-linecap="square" stroke='rgba(0, 0, 0, 0.3)' stroke-width="7"></path>
</g>
<animate attributeName="x" attributeType="XML" begin="0s" dur="0.5s" from="0" to="-20" repeatCount="indefinite"></animate>
</pattern>
<pattern id="device_small_bg" patternUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
<g>
<rect width="20" height="20" class="base_bg_small"></rect>
</g>
</pattern>
<pattern id="device_small_bg_loading" patternUnits="userSpaceOnUse" x="0" y="0" width="5" height="5">
<g>
<rect width="5" height="5" class="loading_bg_small"></rect>
<path d='M0 5L5 0ZM6 4L4 6ZM-1 1L1 -1Z' stroke-linecap="square" stroke='rgba(0, 0, 0, 0.4)' stroke-width="1.5"></path>
</g>
<animate attributeName="x" attributeType="XML" begin="0s" dur="0.5s" from="0" to="-5" repeatCount="indefinite"></animate>
</pattern>
</defs>
</svg>
<svg id="topology_template" display="none">
<g class="router_small device_body">
<g class="ports" pointer-events="none"></g>
<rect rx="3" ry="3" width="20" height="20" class="frame"></rect>
<g transform="translate(3.5,3)" class="icon">
<polygon points="12.51,4.23 12.51,0.49 8.77,0.49 9.68,1.4 6.92,4.16 8.84,6.08 11.6,3.32 "></polygon>
<polygon points="0.49,8.77 0.49,12.51 4.23,12.51 3.32,11.6 6.08,8.83 4.17,6.92 1.4,9.68 "></polygon>
<polygon points="1.85,5.59 5.59,5.59 5.59,1.85 4.68,2.76 1.92,0 0,1.92 2.76,4.68 "></polygon>
<polygon points="11.15,7.41 7.41,7.41 7.41,11.15 8.32,10.24 11.08,13 13,11.08 10.24,8.32 "></polygon>
</g>
</g>
<g class="instance_small device_body">
<g class="ports" pointer-events="none"></g>
<rect rx="3" ry="3" width="20" height="20" class="frame"></rect>
<g transform="translate(5,3)" class="icon">
<rect class="instance_bg" width="10" height="13"></rect>
<rect x="2" y="1" fill="#FFFFFF" width="6" height="2"></rect>
<rect x="2" y="4" fill="#FFFFFF" width="6" height="2"></rect>
<circle class="active" cx="3" cy="10" r="1.3"></circle>
</g>
</g>
<g class="network_container_small">
<rect rx="7" ry="7" width="15" height="200" style="fill: #8541B5;" class="network-rect"></rect>
<text x="250" y="-3" class="network-name" transform="rotate(90 0 0)" pointer-events="none">Network</text>
<text x="0" y="-20" class="network-cidr" transform="rotate(90 0 0)">0.0.0.0</text>
<text x="0" y="-20" class="network-type" transform="rotate(90 0 0)"
data-toggle="tooltip" data-placement="bottom" title="{% trans 'External Network' %}"></text>
</g>
<g class="router_normal device_body">
<g class="ports" pointer-events="none"></g>
<rect class="frame" x="0" y="0" rx="6" ry="6" width="90" height="50"></rect>
<g class="texts" pointer-events="none">
<rect class="texts_bg" x="1.5" y="32" width="87" height="17"></rect>
<text x="45" y="46" class="type">{% trans "Router" %}</text>
<text x="45" y="22" class="name">router</text>
</g>
<g class="icon" transform="translate(6,6)" pointer-events="none">
<circle class="icon_bg" cx="0" cy="0" r="12"></circle>
<g transform="translate(-6.5,-6.5)">
<polygon points="12.51,4.23 12.51,0.49 8.77,0.49 9.68,1.4 6.92,4.16 8.84,6.08 11.6,3.32 "></polygon>
<polygon points="0.49,8.77 0.49,12.51 4.23,12.51 3.32,11.6 6.08,8.83 4.17,6.92 1.4,9.68 "></polygon>
<polygon points="1.85,5.59 5.59,5.59 5.59,1.85 4.68,2.76 1.92,0 0,1.92 2.76,4.68 "></polygon>
<polygon points="11.15,7.41 7.41,7.41 7.41,11.15 8.32,10.24 11.08,13 13,11.08 10.24,8.32 "></polygon>
</g>
</g>
</g>
<g class="instance_normal device_body">
<g class="ports" pointer-events="none"></g>
<rect class="frame" x="0" y="0" rx="6" ry="6" width="90" height="50"></rect>
<g class="texts">
<rect class="texts_bg" x="1.5" y="32" width="87" height="17"></rect>
<text x="45" y="46" class="type">{% trans "Instance" %}</text>
<text x="45" y="22" class="name">instance</text>
</g>
<g class="icon" transform="translate(6,6)">
<circle class="icon_bg" cx="0" cy="0" r="12"></circle>
<g transform="translate(-5,-6.5)">
<rect class="instance_bg" width="10" height="13"></rect>
<rect x="2" y="1" fill="#FFFFFF" width="6" height="2"></rect>
<rect x="2" y="4" fill="#FFFFFF" width="6" height="2"></rect>
<circle class="active" cx="3" cy="10" r="1.3"></circle>
</g>
</g>
</g>
<g class="network_container_normal">
<rect rx="9" ry="9" width="17" height="500" style="fill: #8541B5;" class="network-rect"></rect>
<text x="250" y="-4" class="network-name" transform="rotate(90 0 0)" pointer-events="none">Network</text>
<text x="490" y="-20" class="network-cidr" transform="rotate(90 0 0)">0.0.0.0</text>
<text x="490" y="-20" class="network-type" transform="rotate(90 0 0)"
data-toggle="tooltip" data-placement="bottom" title="{% trans 'External Network' %}"></text>
</g>
</svg>

View File

@ -0,0 +1,20 @@
{% load i18n %}
<div class="topology-navi">
<div class="toggle-view btn-group" data-toggle="buttons">
<label class="btn btn-default" data-value="small">
<input type="radio" name="options" id="option1" checked>
<span class="fa fa-th"></span>
{% trans "Small" %}
</label>
<label class="btn btn-default" data-value="normal">
<input type="radio" name="options" id="option2">
<span class="fa fa-th-large"></span>
{% trans "Normal" %}
</label>
</div>
</div>
<div id="flatTopologyCanvasContainer">
<div class="nodata">{% blocktrans %}There are no networks, routers, or connected instances to display.{% endblocktrans %}</div>
{% include "project/network_topology/_svg_element.html" %}
</div>

View File

@ -13,41 +13,14 @@
{% include "project/network_topology/client_side/_balloon_port.html" %} {% include "project/network_topology/client_side/_balloon_port.html" %}
{% include "project/network_topology/client_side/_balloon_net.html" %} {% include "project/network_topology/client_side/_balloon_net.html" %}
{% include "project/network_topology/client_side/_balloon_instance.html" %} {% include "project/network_topology/client_side/_balloon_instance.html" %}
<div class='description'>
{% blocktrans %} {% include 'project/network_topology/_actions_list.html' %}
Resize the canvas by scrolling up/down with your mouse/trackpad on the topology. <div class="row">
Pan around the canvas by clicking and dragging the space behind the topology. <div class="col-sm-12">
{% endblocktrans %} {{ tab_group.render }}
</div>
<div class="topologyNavi">
<div class="toggleView btn-group" data-toggle="buttons-radio">
<button type="button" class="btn btn-default" id="toggle_labels"><span class="fa
fa-th-large"></span> {%trans "Toggle labels" %}</button>
<button type="button" class="btn btn-default" id="toggle_networks"><span class="fa
fa-th"></span> {%trans "Toggle Network Collapse" %}</button>
</div>
<div class="launchButtons">
{% if launch_instance_allowed %}
{% if show_ng_launch %}
{% url 'horizon:project:network_topology:index' as networkUrl %}
<a href="javascript:void(0);" ng-controller="LaunchInstanceModalController as modal" ng-click="modal.openLaunchInstanceWizard({successUrl: '{{networkUrl}}'})" id="instances__action_launch-ng" class="btn btn-default btn-sm btn-launch {% if instance_quota_exceeded %}disabled{% endif %}"><span class="fa fa-cloud-upload"></span> {% if instance_quota_exceeded %}{% trans "Launch Instance (Quota exceeded)"%}{% else %}{% trans "Launch Instance"%}{% endif %}</a>
{% endif %}
{% if show_legacy_launch %}
<a href="{% url 'horizon:project:network_topology:launchinstance' %}" id="instances__action_launch" class="btn btn-default btn-sm btn-launch ajax-modal {% if instance_quota_exceeded %}disabled{% endif %}"><span class="fa fa-cloud-upload"></span> {% if instance_quota_exceeded %}{% trans "Launch Instance (Quota exceeded)"%}{% else %}{% trans "Launch Instance"%}{% endif %}</a>
{% endif %}
{% endif %}
{% if create_network_allowed %}
<a href="{% url 'horizon:project:network_topology:createnetwork' %}" id="networks__action_create" class="btn btn-default btn-sm ajax-modal {% if network_quota_exceeded %}disabled{% endif %}"><span class="fa fa-plus"></span> {% if network_quota_exceeded %}{% trans "Create Network (Quota exceeded)"%}{% else %}{% trans "Create Network"%}{% endif %}</a>
{% endif %}
{% if create_router_allowed %}
<a href="{% url 'horizon:project:network_topology:createrouter' %}" id="Routers__action_create" class="btn btn-default btn-sm ajax-modal {% if router_quota_exceeded %}disabled{% endif %}"><span class="fa fa-plus"></span> {% if router_quota_exceeded %}{% trans "Create Router (Quota exceeded)"%}{% else %}{% trans "Create Router"%}{% endif %}</a>
{% endif %}
</div> </div>
</div> </div>
<div id="topologyCanvasContainer">
<div class="nodata">{% blocktrans %}There are no networks, routers, or connected instances to display.{% endblocktrans %}</div>
</div>
<span data-networktopology="{% url 'horizon:project:network_topology:json' %}" id="networktopology"></span> <span data-networktopology="{% url 'horizon:project:network_topology:json' %}" id="networktopology"></span>
<div id="topologyMessages"></div> <div id="topologyMessages"></div>
@ -56,6 +29,8 @@ Pan around the canvas by clicking and dragging the space behind the topology.
horizon.network_topology.init(); horizon.network_topology.init();
} else { } else {
addHorizonLoadEvent(function () { addHorizonLoadEvent(function () {
horizon.networktopologycommon.init();
horizon.flat_network_topology.init();
horizon.network_topology.init(); horizon.network_topology.init();
}); });
} }

View File

@ -209,7 +209,7 @@ class NetworkTopologyCreateTests(test.TestCase):
@test.create_stubs({quotas: ('tenant_quota_usages',)}) @test.create_stubs({quotas: ('tenant_quota_usages',)})
def test_create_network_button_disabled_when_quota_exceeded(self): def test_create_network_button_disabled_when_quota_exceeded(self):
url = reverse('horizon:project:network_topology:createnetwork') url = reverse('horizon:project:network_topology:createnetwork')
classes = 'btn btn-default btn-sm ajax-modal' classes = 'btn btn-default ajax-modal'
link_name = "Create Network (Quota exceeded)" link_name = "Create Network (Quota exceeded)"
expected_string = "<a href='%s' class='%s disabled' "\ expected_string = "<a href='%s' class='%s disabled' "\
"id='networks__action_create'>" \ "id='networks__action_create'>" \
@ -222,7 +222,7 @@ class NetworkTopologyCreateTests(test.TestCase):
@test.create_stubs({quotas: ('tenant_quota_usages',)}) @test.create_stubs({quotas: ('tenant_quota_usages',)})
def test_create_router_button_disabled_when_quota_exceeded(self): def test_create_router_button_disabled_when_quota_exceeded(self):
url = reverse('horizon:project:network_topology:createrouter') url = reverse('horizon:project:network_topology:createrouter')
classes = 'btn btn-default btn-sm ajax-modal' classes = 'btn btn-default ajax-modal'
link_name = "Create Router (Quota exceeded)" link_name = "Create Router (Quota exceeded)"
expected_string = "<a href='%s' class='%s disabled' "\ expected_string = "<a href='%s' class='%s disabled' "\
"id='Routers__action_create'>" \ "id='Routers__action_create'>" \
@ -236,7 +236,7 @@ class NetworkTopologyCreateTests(test.TestCase):
@test.create_stubs({quotas: ('tenant_quota_usages',)}) @test.create_stubs({quotas: ('tenant_quota_usages',)})
def test_launch_instance_button_disabled_when_quota_exceeded(self): def test_launch_instance_button_disabled_when_quota_exceeded(self):
url = reverse('horizon:project:network_topology:launchinstance') url = reverse('horizon:project:network_topology:launchinstance')
classes = 'btn btn-default btn-sm btn-launch ajax-modal' classes = 'btn btn-default btn-launch ajax-modal'
link_name = "Launch Instance (Quota exceeded)" link_name = "Launch Instance (Quota exceeded)"
expected_string = "<a href='%s' class='%s disabled' "\ expected_string = "<a href='%s' class='%s disabled' "\
"id='instances__action_launch'>" \ "id='instances__action_launch'>" \

View File

@ -0,0 +1,56 @@
# 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.conf import settings
from openstack_dashboard.usage import quotas
def _has_permission(request, policy):
has_permission = True
policy_check = getattr(settings, "POLICY_CHECK_FUNCTION", None)
if policy_check:
has_permission = policy_check(policy, request)
return has_permission
def _quota_exceeded(request, quota):
usages = quotas.tenant_quota_usages(request)
available = usages.get(quota, {}).get('available', 1)
return available <= 0
def get_context(request, context=None):
"""Returns common context data for network topology views."""
if context is None:
context = {}
network_config = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {})
context['launch_instance_allowed'] = _has_permission(
request, (("compute", "compute:create"),))
context['instance_quota_exceeded'] = _quota_exceeded(request, 'instances')
context['create_network_allowed'] = _has_permission(
request, (("network", "create_network"),))
context['network_quota_exceeded'] = _quota_exceeded(request, 'networks')
context['create_router_allowed'] = (
network_config.get('enable_router', True) and
_has_permission(request, (("network", "create_router"),)))
context['router_quota_exceeded'] = _quota_exceeded(request, 'routers')
context['console_type'] = getattr(settings, 'CONSOLE_TYPE', 'AUTO')
context['show_ng_launch'] = getattr(
settings, 'LAUNCH_INSTANCE_NG_ENABLED', True)
context['show_legacy_launch'] = getattr(
settings, 'LAUNCH_INSTANCE_LEGACY_ENABLED', False)
return context

View File

@ -27,12 +27,10 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import View # noqa from django.views.generic import View # noqa
from horizon import exceptions from horizon import exceptions
from horizon import tabs
from horizon.utils.lazy_encoder import LazyTranslationEncoder from horizon.utils.lazy_encoder import LazyTranslationEncoder
from horizon import views
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.project.network_topology.instances \ from openstack_dashboard.dashboards.project.network_topology.instances \
import tables as instances_tables import tables as instances_tables
from openstack_dashboard.dashboards.project.network_topology.networks \ from openstack_dashboard.dashboards.project.network_topology.networks \
@ -43,6 +41,9 @@ from openstack_dashboard.dashboards.project.network_topology.routers \
import tables as routers_tables import tables as routers_tables
from openstack_dashboard.dashboards.project.network_topology.subnets \ from openstack_dashboard.dashboards.project.network_topology.subnets \
import tables as subnets_tables import tables as subnets_tables
from openstack_dashboard.dashboards.project.network_topology \
import tabs as topology_tabs
from openstack_dashboard.dashboards.project.network_topology import utils
from openstack_dashboard.dashboards.project.instances import\ from openstack_dashboard.dashboards.project.instances import\
console as i_console console as i_console
@ -183,45 +184,14 @@ class NetworkDetailView(n_views.DetailView):
template_name = 'project/network_topology/iframe.html' template_name = 'project/network_topology/iframe.html'
class NetworkTopologyView(views.HorizonTemplateView): class NetworkTopologyView(tabs.TabView):
tab_group_class = topology_tabs.TopologyTabs
template_name = 'project/network_topology/index.html' template_name = 'project/network_topology/index.html'
page_title = _("Network Topology") page_title = _("Network Topology")
def _has_permission(self, policy):
has_permission = True
policy_check = getattr(settings, "POLICY_CHECK_FUNCTION", None)
if policy_check:
has_permission = policy_check(policy, self.request)
return has_permission
def _quota_exceeded(self, quota):
usages = quotas.tenant_quota_usages(self.request)
available = usages.get(quota, {}).get('available', 1)
return available <= 0
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(NetworkTopologyView, self).get_context_data(**kwargs) context = super(NetworkTopologyView, self).get_context_data(**kwargs)
network_config = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}) return utils.get_context(self.request, context)
context['launch_instance_allowed'] = self._has_permission(
(("compute", "compute:create"),))
context['instance_quota_exceeded'] = self._quota_exceeded('instances')
context['create_network_allowed'] = self._has_permission(
(("network", "create_network"),))
context['network_quota_exceeded'] = self._quota_exceeded('networks')
context['create_router_allowed'] = (
network_config.get('enable_router', True) and
self._has_permission((("network", "create_router"),)))
context['router_quota_exceeded'] = self._quota_exceeded('routers')
context['console_type'] = getattr(
settings, 'CONSOLE_TYPE', 'AUTO')
context['show_ng_launch'] = getattr(
settings, 'LAUNCH_INSTANCE_NG_ENABLED', True)
context['show_legacy_launch'] = getattr(
settings, 'LAUNCH_INSTANCE_LEGACY_ENABLED', False)
return context
class JSONView(View): class JSONView(View):

View File

@ -1,4 +1,4 @@
#topologyCanvasContainer { #topologyCanvasContainer, #flatTopologyCanvasContainer {
@include box-sizing(border-box); @include box-sizing(border-box);
width: 100%; width: 100%;
height: auto; height: auto;
@ -24,10 +24,10 @@
} }
} }
.topologyNavi { .topology-navi {
overflow: hidden; overflow: hidden;
margin: 10px 0; margin: 10px 0;
.toggleView { .toggle-view {
float: left; float: left;
span.glyphicon { span.glyphicon {
margin-right: 4px; margin-right: 4px;

View File

@ -46,6 +46,8 @@
<script src='{{ STATIC_URL }}horizon/js/horizon.users.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.users.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.membership.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.membership.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.metering.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.metering.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.networktopologycommon.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.flatnetworktopology.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.networktopology.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.networktopology.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.d3piechart.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.d3piechart.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.heattop.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.heattop.js'></script>