diff --git a/horizon/static/horizon/js/horizon.flatnetworktopology.js b/horizon/static/horizon/js/horizon.flatnetworktopology.js new file mode 100644 index 0000000000..f2772f9a5f --- /dev/null +++ b/horizon/static/horizon/js/horizon.flatnetworktopology.js @@ -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; + } + } +}; diff --git a/horizon/static/horizon/js/horizon.networktopology.js b/horizon/static/horizon/js/horizon.networktopology.js index a7af045af0..0006bb7dd7 100644 --- a/horizon/static/horizon/js/horizon.networktopology.js +++ b/horizon/static/horizon/js/horizon.networktopology.js @@ -53,7 +53,6 @@ function Server(data) { } horizon.network_topology = { - model: null, fa_globe_glyph: '\uf0ac', fa_globe_glyph_width: 15, svg:'#topology_canvas', @@ -63,7 +62,6 @@ horizon.network_topology = { zoom: d3.behavior.zoom(), data_loaded: false, svg_container:'#topologyCanvasContainer', - post_messages:'#topologyMessages', balloonTmpl : null, balloon_deviceTmpl : null, balloon_portTmpl : null, @@ -71,10 +69,7 @@ horizon.network_topology = { balloon_instanceTmpl : null, network_index: {}, balloonID:null, - reload_duration: 10000, network_height : 0, - previous_message : null, - deleting_device : null, init:function() { var self = this; @@ -136,53 +131,49 @@ horizon.network_topology = { 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); self.create_vis(); self.loading(); self.force_direction(0.05,70,-700); - self.retrieve_network_info(true); + if(horizon.networktopologyloader.model !== null) { + 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 retrieve_network_info: function(force_start) { 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.load_topology(data); - if (force_start) { - var i = 0; - self.force.start(); - while (i <= 100) { - self.force.tick(); - i++; - } - } - setTimeout(function() { - self.retrieve_network_info(); - }, self.reload_duration); + self.data_loaded = true; + self.load_topology(horizon.networktopologyloader.model); + if (force_start) { + var i = 0; + self.force.start(); + while (i <= 100) { + self.force.tick(); + i++; } - ); + } }, // Load config from cookie @@ -222,7 +213,7 @@ horizon.network_topology = { // Main svg self.outer_group = d3.select('#topologyCanvasContainer').append('svg') .attr('width', '100%') - .attr('height', angular.element(document).height() - 200 + "px") + .attr('height', angular.element(document).height() - 270 + "px") .attr('pointer-events', 'all') .append('g') .call(self.zoom @@ -837,17 +828,14 @@ horizon.network_topology = { }, delete_device: function(type, deviceId) { - var self = this; var message = {id:deviceId}; - self.post_message(deviceId,type,message); - self.deleting_device = {type: type, deviceId: deviceId}; + horizon.networktopologymessager.post_message(deviceId,type,message,type,'delete',data={}); }, - remove_node_on_delete: function () { + remove_node_on_delete: function(deleteData) { var self = this; - var type = self.deleting_device.type; - var deviceId = self.deleting_device.deviceId; - switch (type) { + var deviceId = deleteData.device_id; + switch (deleteData.device_type) { case 'router': self.removeNode(self.data.routers[deviceId]); break; @@ -858,15 +846,18 @@ horizon.network_topology = { case 'network': self.removeNode(self.data.networks[deviceId]); break; + case 'port': + self.removePort(deviceId, deleteData.device_data); + break; } self.delete_balloon(); }, - delete_port: function(routerId, portId, networkId) { + removePort: function(portId, deviceData) { var self = this; - var message = {id:portId}; + var routerId = deviceData.router_id; + var networkId = deviceData.network_id; if (routerId) { - self.post_message(portId, 'router/' + routerId + '/', message); for (var l in self.links) { var data = null; 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) { data = self.links[l].target.data; } - if (data) { for (var p in data.ports) { 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 { - 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: return ''; } - }, - - post_message: function(id,url,message) { - var self = this; - var iframeID = 'ifr_' + id; - var iframe = angular.element('