From 067fe18168fae8b5cf5ac211dcf1f52f476e8bb7 Mon Sep 17 00:00:00 2001 From: decheng zhang Date: Wed, 6 Oct 2021 21:07:43 -0400 Subject: Display CCVPN network topology and tunnel installation. Issue-ID: REQ-1103 Signed-off-by: decheng zhang Change-Id: Ia8ad763ef8cdd48238c1398bd6680cc8ea2d14d6 Signed-off-by: decheng zhang --- .../ccvpn-network/ccvpn-network.component.css | 11 +- .../ccvpn-network/ccvpn-network.component.html | 82 +- .../ccvpn-network/ccvpn-network.component.ts | 2122 ++++++++++++-------- 3 files changed, 1318 insertions(+), 897 deletions(-) (limited to 'usecaseui-portal/src/app/views/network/ccvpn-network') diff --git a/usecaseui-portal/src/app/views/network/ccvpn-network/ccvpn-network.component.css b/usecaseui-portal/src/app/views/network/ccvpn-network/ccvpn-network.component.css index e1a1d6e4..e2abdfc5 100644 --- a/usecaseui-portal/src/app/views/network/ccvpn-network/ccvpn-network.component.css +++ b/usecaseui-portal/src/app/views/network/ccvpn-network/ccvpn-network.component.css @@ -1,5 +1,6 @@ /* Copyright (C) 2019 CMCC, Inc. and others. All rights reserved. + Copyright (C) 2022 Huawei Canada Limited. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -36,11 +37,19 @@ #tpContainer{ position: relative; width:100%; - height: 95%; + height: 65%; margin-top: 15px; float: left; background: #EEF9FF; } + +.slicing-resource-table{ + position: relative; + width: 100%; + margin-top: 500px; + +} + #tpContainer .no-network{ width: 300px; margin: 0 auto; diff --git a/usecaseui-portal/src/app/views/network/ccvpn-network/ccvpn-network.component.html b/usecaseui-portal/src/app/views/network/ccvpn-network/ccvpn-network.component.html index 48d01d36..c9e150fa 100644 --- a/usecaseui-portal/src/app/views/network/ccvpn-network/ccvpn-network.component.html +++ b/usecaseui-portal/src/app/views/network/ccvpn-network/ccvpn-network.component.html @@ -1,5 +1,6 @@ -

+

Network Topology:

-
+

There is not any terminal device can be used for configuration @@ -65,6 +76,55 @@

No network available

+
+
+ + + + Service Instance Id + Service Type + VLAN + Bandwidth + Status + + + + + + {{ data.id }} + Cloud Lease Line + {{ data.vlan }} + {{ data.bw }} + + Activated + + + + + +
+
+
diff --git a/usecaseui-portal/src/app/views/network/ccvpn-network/ccvpn-network.component.ts b/usecaseui-portal/src/app/views/network/ccvpn-network/ccvpn-network.component.ts index d1e40672..6acdfa66 100644 --- a/usecaseui-portal/src/app/views/network/ccvpn-network/ccvpn-network.component.ts +++ b/usecaseui-portal/src/app/views/network/ccvpn-network/ccvpn-network.component.ts @@ -1,5 +1,6 @@ /* Copyright (C) 2019 CMCC, Inc. and others. All rights reserved. + Copyright (C) 2022 Huawei Canada Limited. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,13 +14,46 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Component, OnInit} from '@angular/core'; +import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'; import * as d3 from 'd3' import * as $ from 'jquery'; import {networkHttpservice} from '../../../core/services/networkHttpservice.service'; -import {EventQueueService} from "../../../core/services/eventQueue.service"; -import {AppEvent} from "@src/app/core/services/appEvent"; -import {AppEventType} from "@src/app/core/services/appEventType"; + +// Customizable colors for edge, domain, node and font +const DOMAIN_COLOR = 'lightcyan' +const NODE_COLOR = 'DeepSkyBlue' +const CE_COLOR = 'Gray' +const FONT_COLOR = 'Navy' +const TITLE_COLOR = '#0da9e2' //'linear-gradient(90deg, #07a9e1 0%, #30d9c4 100%)' + +// Customizable colors for endpoint CRUD status +const EP_COLOR_MAP = new Map([ + ['create', 'RoyalBlue'], + ['retrieve', 'ForestGreen'], + ['update', 'orange'], + ['delete', 'red'], +]) + +enum NodePosition { + L2_NODE_POS = 'l2_node_pos', +} + +declare var mxGraph: any; +declare var mxCell: any; +declare var mxHierarchicalLayout: any; +declare var mxRubberband: any; +declare var mxKeyHandler: any; +declare var mxConstants: any; +declare var mxCellRenderer: any; +declare var mxVertexHandler: any; +declare var mxGraphHandler: any; +declare var mxGraphSelectionModel: any; +declare var mxFastOrganicLayout: any; +declare var mxStackLayout: any; +declare var mxParallelEdgeLayout: any; +declare var mxPerimeter: any; +declare var mxEdgeStyle: any; +declare var mxAbstractCanvas2D: any; @Component({ selector: 'app-ccvpn-network', @@ -28,63 +62,305 @@ import {AppEventType} from "@src/app/core/services/appEventType"; }) export class CcvpnNetworkComponent implements OnInit { - constructor(private myhttp: networkHttpservice, - private eventDispatcher: EventQueueService) { - } + @ViewChild('tpContainer') graphContainer: ElementRef; + @ViewChild('tableContainer') tableContainer: ElementRef; + + constructor(private myhttp: networkHttpservice) {} ngOnInit() { + } + + reqNumber = 0 + controllers = [] + onap = {} + domainMap = new Map() + enniMap = new Map() + sliceMap = new Map() + tunnelsMap = new Map() + e2eTunnels = [] + e2eTunnelMap = new Map() + servicesMap = new Map() + e2eServices = [] + e2eServiceMap = new Map() + defBandwidth = 1 + currentLayer = 1 + currentCloud = '' + currentSlice = 'Physical' + isNodeName = true + isMoreLabels = true + storage = window.localStorage + + graph = null; + gLayers = []; + graphScale = 1; + tunnelTable = null; + serviceTable = null; + edgeLayout = null; + organicLayout = null; + stackLayout = null; + + // Constants + readonly DOMAIN_STYLE = 'fillColor=' + DOMAIN_COLOR + ';shape=rectangle;strokeColor=none;gradientColor=none;' + + 'verticalLabelPosition=top;verticalAlign=bottom;autosize=1;resizable=1;rounded=1;opacity=50;fontStyle=1'; + readonly CPE_STYLE = 'fillColor=' + CE_COLOR + ';shape=rectangle;rounded=1'; + readonly CLOUD_STYLE = 'fillColor=' + CE_COLOR + ';shape=cloud'; + readonly LINK_STYLE = 'strokeWidth=3;edgeStyle=null' + readonly UNI_LINK_STYLE = 'strokeWidth=2;edgeStyle=null;strokeColor=' + CE_COLOR + readonly TUNNEL_STYLE = 'strokeWidth=2;curved=1' + readonly SERVICE_STYLE = 'strokeWidth=1;curved=1' + + ngAfterViewInit() { let thisNg = this; - this.isSpinning = true; - this.myhttp.getConnectivities() - .subscribe((data) => { - if(data){ - for (let conn of data["connectivity"]) { - if (conn["vpn-type"] === "mdsc"){ - this.connectivityList.push({ "name": conn["connectivity-id"], - "id": conn["connectivity-id"], - "relationship-list" : conn["relationship-list"] - }); - } + this.graph = new mxGraph(this.graphContainer.nativeElement); + this.graph.setPanning(true) + this.graph.setTooltips(true) + this.graph.setHtmlLabels(true) + this.graph.cellsDisconnectable = false + this.graph.cellsEditable = false + this.graph.cellsCloneable = false + this.graph.foldingEnabled = false + this.graph.edgeLabelsMovable = false + this.graph.autoExtend = false + this.graph.gridEnabled = false + this.graph.model.maintainEdgeParent = false; + this.graphScale = 1; + + new mxRubberband(this.graph); + new mxKeyHandler(this.graph); + //mxLog.show = () => { } + mxConstants.VERTEX_SELECTION_STROKEWIDTH = 1 + mxConstants.EDGE_SELECTION_STROKEWIDTH = 5 + // mxConstants.EDGE_SELECTION_DASHED = false + mxConstants.LOCKED_HANDLE_FILLCOLOR = 'none' + mxConstants.HANDLE_STROKECOLOR = 'none' + mxConstants.INVALID_COLOR = '#000000' + // Keeps the font sizes independent of the scale + mxCellRenderer.prototype.getTextScale = function (state) { + return 1 + } + mxVertexHandler.prototype.constrainGroupByChildren = true + mxGraphHandler.prototype.maxLivePreview = 16 + mxGraphHandler.prototype.removeCellsFromParent = false + mxGraphHandler.prototype.isPropagateSelectionCell = + function (cell, immediate, me) { + return false + } + const CELL_ADDED = mxGraphSelectionModel.prototype.cellAdded + mxGraphSelectionModel.prototype.cellAdded = function (cell) { + CELL_ADDED.call(this, cell) + if (cell.isEdge()) this.addCells([cell.source, cell.target]) + } + const CELL_REMOVED = mxGraphSelectionModel.prototype.cellRemoved + mxGraphSelectionModel.prototype.cellRemoved = function (cell) { + CELL_REMOVED.call(this, cell) + if (cell.isVertex()) { + this.removeCells(cell.edges) + } + } + + // Creates a layout algorithm to be used with the graph + this.organicLayout = new mxFastOrganicLayout(this.graph); + // Moves stuff wider apart than usual 50 + this.organicLayout.forceConstant = 80; + this.stackLayout = new mxStackLayout(this.graph) + this.edgeLayout = new mxParallelEdgeLayout(this.graph) + this.edgeLayout.spacing = 15 + + // Sets default vertex style + this.setObjValues(this.graph.stylesheet.getDefaultVertexStyle(), { + STYLE_SHAPE: mxConstants.SHAPE_ELLIPSE, + STYLE_PERIMETER: mxPerimeter.EllipsePerimeter, + STYLE_FILLCOLOR: NODE_COLOR, + STYLE_GRADIENTCOLOR: 'white', + STYLE_STROKECOLOR: '#1B78C8', + STYLE_FONTCOLOR: FONT_COLOR, + STYLE_FONTSIZE: '14', + STYLE_VERTICAL_LABEL_POSITION: 'bottom', + STYLE_VERTICAL_ALIGN: 'top', + STYLE_RESIZABLE: '0', + }, mxConstants) + + // Sets default edge style + this.setObjValues(this.graph.stylesheet.getDefaultEdgeStyle(), { + STYLE_FONTCOLOR: 'black', + STYLE_FONTSIZE: '14', + STYLE_STROKECOLOR: 'black', + STYLE_EDGE: mxEdgeStyle.TopToBottom, + STYLE_ENDARROW: 'none', + STYLE_LABEL_BACKGROUNDCOLOR: 'white', + STYLE_TEXT_OPACITY: '70', + }, mxConstants) + + // Gets label from custom user object + // TODO: + this.graph.convertValueToString = function (cell) { + if (cell.isEdge() && !cell.value.uni && !this.isMoreLabels) return '' + return (cell.value && cell.value.label) ? cell.value.label : ''; + } + + // Installs a custom tooltip for cells + this.graph.getTooltipForCell = function (cell) { + let tooltip = '' + for (let key of Object.keys(cell.value).sort()) { + if (key === 'label' || cell.value[key] === '' || key[0] == '$' || + !thisNg.isBasicType(cell.value[key])) continue + tooltip += '' + key + ': ' + cell.value[key] + '\n' + } + return tooltip + } + + // Installs a popupmenu handler. + this.graph.popupMenuHandler.factoryMethod = this.createPopupMenu + document.body.onmousedown = function () { + let popupMenu = document.body.getElementsByClassName('mxPopupMenu') + if (popupMenu.length) document.body.removeChild(popupMenu[0]) + } + + var dragStatus = 0 + // Listen to the Mouseup event to update table and json data view + this.graph.addMouseListener({ + mouseDown: function (sender, evt) { + dragStatus = 1 + }, + mouseMove: function (sender, evt) { + if (dragStatus == 1) dragStatus = 2 + }, + mouseUp: function (sender, evt) { + if (dragStatus == 2) { + if (sender.getSelectionCount() >= 1) { + this.clientNodeLabelLayout() } - if (this.connectivityList.length !== 0) { - this.connectivitySelected = this.connectivityList[0]; - this.choseConnectivity(this.connectivitySelected); + } + dragStatus = 0 - }; + this.deselectTableRow() + let cell = null + if (!evt.evt.defaultPrevented) { + if (!evt.state) return + else cell = evt.state.cell + } else { + if (sender.getSelectionCount() == 1) { + cell = sender.getSelectionCell() + } else if (sender.getSelectionCount() == 3) { + for (let item of sender.getSelectionCells()) { + if (item.isEdge()) { + if (!cell) cell = item + else { + cell = null; + break + } + } + } + } } - }, - (err) => { - console.log(err); - }); + if (!cell || !cell.value) { + return + } + let obj = null + if (obj = cell.value.controller) { + this.showJsonData([obj], false) + } else if (obj = cell.value.node) { + this.showJsonData(obj.uniSliceMap.get(this.currentSlice), false) + } else if (obj = cell.value.enni) { + this.showJsonData([obj.data[0].link.data, obj.data[1].link.data], false) + } else if (obj = cell.value.inni) { + this.showJsonData([obj.data[0].data, obj.data[1].data], false) + } else if (obj = cell.value.uni) { + this.showJsonData(obj.data, false) + } else if (obj = cell.value.tunnel) { + this.selectTableRow(cell.value.index) + this.showJsonData(this.tunnelsMap.get(obj.name), false) + } else if (obj = cell.value.service) { + this.selectTableRow(cell.value.index) + this.showJsonData(this.servicesMap.get(obj.name), false) + } + } + }) + + this.isSpinning = true; + let reqCount = 0; + + reqCount++; this.myhttp.getLogicalLinksData() .subscribe((data) => { if (data) { - for (let ll of data["logical-link"]){ - // Filter layer1 logical link - //if (ll["relationship-list"] !== undefined && - // ll["relationship-list"]["relationship"].length) { + for (let ll of data["logical-link"]) { thisNg.logicalLinks.push(ll); - //} } - let tpMapping = thisNg.getPnfTpMapping(thisNg.logicalLinks); + } + if (--reqCount == 0) { + thisNg.finishNetworkView(); + this.isSpinning = false; + } + }, + (err) => { + console.log(err); + }) - let links = thisNg.getLinks( thisNg.logicalLinks, tpMapping); - let tps = thisNg.getNodes(tpMapping); - console.log(links); - console.log(tps); + reqCount++; + this.myhttp.getPnfsData() + .subscribe((data) => { + if (data) { + for (let ll of data["pnf"]) { + thisNg.pnfs.push(ll); + } + } + if (--reqCount == 0) { + thisNg.finishNetworkView(); + this.isSpinning = false; + } + }, + (err) => { + console.log(err); + }) - thisNg.drawTopo(tps, links); + reqCount++; + this.myhttp.getConnectivities() + .subscribe((data) => { + if (data) { + for (let ll of data["connectivity"]) { + thisNg.connectivities.push(ll); + } + } + if (--reqCount == 0) { + thisNg.finishNetworkView(); + this.isSpinning = false; + } + }, + (err) => { + console.log(err); + }) + reqCount++; + this.myhttp.getNetworkRoutes() + .subscribe((data) => { + if (data) { + for (let ll of data["network-route"]) { + thisNg.networkroutes.push(ll); + } + } + if (--reqCount == 0) { + thisNg.finishNetworkView(); + this.isSpinning = false; } - this.isSpinning = false; }, (err) => { console.log(err); - }) + }) } connectivityList = []; - connectivitySelected = { name: null, id: null }; + connectivitySelected = {name: null, id: null}; + serviceGraphModel: { [k: string]: any } = {}; + + layerList = [ + { value: 3, name: 'Service layer' }, + { value: 2, name: 'Tunnel layer' }, + { value: 1, name: 'Link layer' }] + layerSelected = { value: 1, name: 'Link layer' } + serviceList = []; + serviceSelected = ''; + addLinkDisabled = true; nonetwork = false; @@ -95,10 +371,14 @@ export class CcvpnNetworkComponent implements OnInit { isSpinning = true; pnfs = []; + logicalLinks = [];//logicalLinks Existing connection data returned by the interface + connectivities = []; + networkroutes = []; + layer1Tps = []; d3Data = [];//D3Render the required data - logicalLinks = [];//logicalLinks Existing connection data returned by the interface + linkName = null;//Linked name link-name networkOption = [];//Form network drop-down box filled data nodeOption1 = {};//Node drop-down box filled data @@ -139,19 +419,8 @@ export class CcvpnNetworkComponent implements OnInit { charge = -300; SEPERATOR = '-'; - - imgMap = { - 'pnf': 'assets/images/site.png', - 'tp': 'assets/images/tp.png' - }; - - //### SELECTION - store the selected node ### - //### EDITING - store the drag mode (either 'drag' or 'add_link') ### - svcEditorGlobal = { - selection: null - } svcContainerOpt = { - containerId : "svcContainer", + containerId: "svcContainer", width: 1000, height: this.winHeight }; @@ -163,907 +432,990 @@ export class CcvpnNetworkComponent implements OnInit { height: this.winHeight }; - /** - * Redraw the selected L2 ethernet service. - * @param {Array} treeData parsed from AAI connectivity. - */ - drawService(treeData) { - //Model of service graph - let graph = { - nodes: [ - ], - links: [ - ], - objectify: (function() { - /* resolve node IDs (not optimized at all!) - */ - var l, n, _i, _len, _ref, _results; - _ref = graph.links; - _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - l = _ref[_i]; - _results.push((function() { - var _j, _len2, _ref2, _results2; - _ref2 = graph.nodes; - _results2 = []; - for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { - n = _ref2[_j]; - if (l.source === n.id) { - l.source = n; - continue; - } - if (l.target === n.id) { - l.target = n; - continue; - } else { - _results2.push(void 0); + finishNetworkView() { + this.updateTopoData(); + console.log(this.domainMap); + this.finishSotnView(); + } + + updateTopoData(){ + let thisNg = this; + // Update the network topo data + // Update node data + for (let pnf of thisNg.pnfs){ + let pnfId = pnf["pnf-id"]; + let arr = pnfId.split('.'); + let domainId = arr[1]; + let domain = thisNg.domainMap.get(domainId); + if (!domain){ + let sotnDomain = { + domainId : domainId, + nodeMap: new Map(), + localLinkMap: new Map(), + inniMap: new Map(), + uniMap: new Map(), + clientNodeMap: new Map() + } + thisNg.domainMap.set(domainId, sotnDomain); + domain = sotnDomain; + } + let node = { + id: pnfId, + name: pnfId + } + domain.nodeMap.set(pnfId, node); + } + + // Update serive data + for (let cn of this.connectivities){ + if (cn['vpn-type'] === "mdsc"){ + let svcInstId = this.getValueFromRelationList(cn, "service-instance", "service-instance.service-instance-id"); + let bw = cn["bandwidth-profile-name"]; + let cvlan = cn["cvlan"]; + let svc = this.servicesMap.get(svcInstId); + if (!svc){ + svc = { + name: svcInstId, + id: svcInstId, + connections: [], + bw : bw, + vlan : cvlan + } + this.servicesMap.set(svcInstId, svc); + } + if (!this.serviceSelected){ + this.serviceSelected = svcInstId; + } + } + } + // Update link data + for (let ll of thisNg.logicalLinks) { + let linkName = ll["link-name"]; + let linkType = ll["link-type"]; + let linkRole = ll["link-role"]; + let linkId = ll["link-id"]; + + if (linkName.search("topologyId-2") >= 0) { + // uni links + let arr = linkId.split('-'); + let arr1 = arr[0].split('.'); + let domainId = arr1[1]; + let domain = thisNg.domainMap.get(domainId); + let remoteNode = domain.nodeMap.get(arr[0]); + if (!remoteNode) continue; + let cpeNode = { + networkNode: remoteNode, + isCloud: arr[1].length <= 3 ? true: false + } + + if (!domain) continue; + domain.clientNodeMap.set(linkId, cpeNode); + let uni = { + srcNodeId: arr[0], + srcUniTp: arr[1], + dstNode: cpeNode + } + domain.uniMap.set(linkId, uni) + } else if (linkRole && linkRole.search("cross-domain") >= 0){ + // enni link + let localLink: Array = new Array(); + let domainLocal:any = null; + let linkId = this.getJsonValue(ll, 'link-id'); + let rlArr: Array = this.getJsonValue(ll, 'relationship-list.relationship'); + for (let rl of rlArr){ + + if (rl['related-to'] === "p-interface"){ + let pnfNameS: String; + let tpNameS: String; + for (let rld of rl['relationship-data']){ + if (rld['relationship-key'] === 'p-interface.interface-name'){ + let tpNameL = rld['relationship-value']; + let tpNameArr = tpNameL.split('-') + tpNameS = tpNameArr[tpNameArr.length - 1]; + pnfNameS = tpNameArr[tpNameArr.length - 3]; + if (!domainLocal){ + let arr = pnfNameS.split('.'); + let domainId = arr[1]; + domainLocal = thisNg.domainMap.get(domainId); + } } } - return _results2; - })()); + let end = { + pnfId : pnfNameS, + tpId: tpNameS + } + localLink.push(end); + } } - return _results; - }), - remove: (function(condemned) { - /* remove the given node or link from the graph, also deleting dangling links if a node is removed - */ if (Array.prototype.indexOf.call(this.nodes, condemned) >= 0) { - this.nodes = this.nodes.filter(function(n) { - return n !== condemned; - }); - return this.links = this.links.filter(function(l) { - return l.source.id !== condemned.id && l.target.id !== condemned.id; - }); - } else if (Array.prototype.indexOf.call(this.links, condemned) >= 0) { - return this.links = this.links.filter(function(l) { - return l !== condemned; - }); + this.enniMap.set(linkId, {data: localLink}); + } else if (linkType.search("Tsci") >= 0) { + // tunnel connection link + let srcEpId = this.getJsonValue(ll, "link-name"); + let dstEpId = this.getJsonValue(ll, "link-name2"); + let svcInstId = this.getValueFromRelationList(ll, "allotted-resource", "service-instance.service-instance-id"); + let conn = { + srcEpId: srcEpId, + dstEpId: dstEpId, + dstEpLtpId: '', + srcEpLtpId: '' } - }), - last_index: 0, - add_node: (function(type) { - var n; - n = { - id: this.last_index++, - x: 960 / 2, - y: 500 / 2, - type: type - }; - this.nodes.push(n); - return n; - }), - add_link: (function(source, target) { - /* avoid links to self - */ - var l, link, _i, _len, _ref; - if (source === target) return null; - /* avoid link duplicates - */ - _ref = this.links; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - link = _ref[_i]; - if (link.source === source && link.target === target) return null; + for (let nr of this.networkroutes){ + if (nr['route-id'] === srcEpId){ + conn.srcEpLtpId = nr['next-hop']; + } + if (nr['route-id'] === dstEpId){ + conn.dstEpLtpId = nr['next-hop']; + } } - l = { - source: source, - target: target - }; - this.links.push(l); - return l; - }) - }; - - var nodeList = treeData.map(obj => { - let rObj = {}; - rObj["id"] = obj["id"]; - rObj["x"] = 500; - rObj["y"] = 500; - rObj["type"] = obj["type"]; - return rObj; - }) - - var linkList = [] ; - for (var i = 0, e = treeData.length; i < e; i++){ - for (var j = i+1, k = e; j < k; j++){ - linkList.push({ - source: treeData[i].id, - target: treeData[j].id - }); + let svc = this.servicesMap.get(svcInstId); + if (svc){ + svc.connections.push(conn); } - } - graph.nodes = nodeList; - graph.links = linkList; - graph.objectify(); - var _this = this; - var margin = {top: 20, right: 120, bottom: 20, left: 120}, - width = 1000 - margin.right - margin.left, - height = 350 - margin.top - margin.bottom; - //clean existing element - d3.select("div#" + this.svcContainerOpt.containerId).selectAll("*").remove(); - - let svg = d3.select("div#" + this.svcContainerOpt.containerId).append("svg") - .attr("width", width + margin.right + margin.left) - .attr("height", height + margin.top + margin.bottom); - let container = svg.append("g").style("fill", "transparent"); - - let vis = container.append('g'); - container.call(d3.behavior.zoom().scaleExtent([0.5, 8]) - .on('zoom', function(){ - vis.attr('transform', "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); - })); - - vis.append('rect') - .attr('class', 'overlay') - .attr('x', -500000) - .attr('y', -500000) - .attr('width', 1000000) - .attr('height', 1000000) - .on('click', function(d) { - _this.svcEditorGlobal.selection = null; - d3.selectAll('.node').classed('selected', false); - return d3.selectAll('.link').classed('selected', false); - }); - let colorify = d3.scale.category10(); - /* initialize the force layout - */ - let force = d3.layout.force().size([width, height]).charge(-400).linkDistance(160) - .on('tick', (function(e) { - /* update nodes and links - */ - let k = 16 * e.alpha; - graph.nodes.forEach(function(o, i) { - if (o["type"] === "root"){ - o["x"] += k - //o["x"] += i & 2 ? k : -k; - - } else if (o["type"] === "leaf") { - o["x"] += -k; - //o["x"] += i & 2 ? k : -k; + } else { + // local link + let localLink: Array = new Array(); + let domainLocal:any = null; + let linkId = this.getJsonValue(ll, 'link-id'); + let rlArr: Array = this.getJsonValue(ll, 'relationship-list.relationship'); + for (let rl of rlArr){ + + if (rl['related-to'] === "p-interface"){ + let pnfNameS: String; + let tpNameS: String; + for (let rld of rl['relationship-data']){ + if (rld['relationship-key'] === 'p-interface.interface-name'){ + let tpNameL = rld['relationship-value']; + let tpNameArr = tpNameL.split('-') + tpNameS = tpNameArr[tpNameArr.length - 1]; + pnfNameS = tpNameArr[tpNameArr.length - 3]; + if (!domainLocal){ + let arr = pnfNameS.split('.'); + let domainId = arr[1]; + domainLocal = thisNg.domainMap.get(domainId); + } + } + } + let end = { + pnfId : pnfNameS, + tpId: tpNameS + } + localLink.push(end); } - }); - vis.selectAll('.node').attr('transform', function(d) { - return "translate(" + d.x + "," + d.y + ")"; - }); - - - //#svcContainer > svg > g > g > g:nth-child(3) > text - //_this.svcEditorGlobal.selection - return vis.selectAll('.link').attr('x1', function(d) { - return d.source.x; - }).attr('y1', function(d) { - return d.source.y; - }).attr('x2', function(d) { - return d.target.x; - }).attr('y2', function(d) { - return d.target.y; - }); - })); - let nodeDragging = force.drag().on('dragstart', function (d){ - d3.event.sourceEvent.stopPropagation(); - d.fixed = true; - }) - - let topoNodeSync = _this.eventDispatcher.on(AppEventType.UserNodeDrag) - .subscribe(event => { - //console.log(event); - let pnfId: string = event.payload.id; - let pnfId_short: string = pnfId.substr(pnfId.lastIndexOf('-')+1); - vis.selectAll('.node > circle').attr('stroke-width', function(d) { - if (d.id.startsWith(pnfId_short)){ - return "4px"; + } + localLink.sort(function(a, b){ + return (a.pnfId + '-' + a.tpId).localeCompare(b.pnfId + '-' + b.tpId)}); + if (!domainLocal) continue; + let srcNodeId = localLink[0].pnfId; + let srcTpId = localLink[0].tpId; + let srcNode = domainLocal.nodeMap.get(srcNodeId); + let dstNodeId = localLink[1].pnfId; + let dstTpId = localLink[1].tpId; + let dstNode = domainLocal.nodeMap.get(dstNodeId); + let inni = domainLocal.inniMap.get(srcNodeId + '-' + srcTpId); + if (!inni){ + let inni = { + data: [linkId, null], + srcNode: srcNode, + srcTpId: srcTpId, + dstNode: dstNode, + dstTpId: dstTpId } - return "1px"; - }); - }); - - // DELETION - pressing DEL deletes the selection - // CREATION - pressing N creates a new node - // d3.select(window).on('keydown', function() { - // if (d3.event.keyCode === 46) { - // if (global.selection != null) { - // graph.remove(global.selection); - // global.selection = null; - // return update(); - // } - // } else if (d3.event.keyCode === 78) { - // graph.add_node(); - // return update(); - // } - // }); - - //Parameter for Editing tools - let toolbar = $("
"); - $("div#" + this.svcContainerOpt.containerId).append(toolbar); - toolbar.append($("\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "")); - toolbar.append($("\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "")); - toolbar.append($("\n" + - "\n" + - " \n" + - " \n \n" + - " \n" + - " \n" + - "")); - let library = $("
"); - toolbar.append(library); - - ['PON', 'ETH'].forEach(function(type) { - var new_btn; - new_btn = $("\n" + - " \n" + - " \n" + - " " + (type) + " UNI \n" + - " " + - " \n" + - ""); - new_btn.bind('click', function() { - graph.add_node(type); - return update(); - }); - library.append(new_btn); - return library.hide(); - }); - - let tool = 'pointer'; - let new_link_source = null; - let drag_link; - vis.on('mousemove.add_link', (function(d) { - /* check if there is a new link in creation - */ - var p; - if (new_link_source != null) { - /* update the draggable link representation - */ - p = d3.mouse(vis.node()); - return drag_link.attr('x1', new_link_source.x).attr('y1', new_link_source.y).attr('x2', p[0]).attr('y2', p[1]); + domainLocal.inniMap.set(srcNodeId + '-' + srcTpId, inni); + } else { + inni.data[1] = linkId; + } } - })).on('mouseup.add_link', (function(d) { - new_link_source = null; - /* remove the draggable link representation, if exists - */ - if (drag_link != null) return drag_link.remove(); - })); - d3.selectAll('.tool').on('click', function() { - var new_tool, nodes; - d3.selectAll('.tool').classed('active', false).style("fill", "#a3a4c3"); - d3.select(this).classed('active', true).style("fill", "#b52d0c"); - new_tool = $(this).data('tool'); - nodes = vis.selectAll('.node'); - - //mode change to add_link - if (new_tool === 'add_link' && tool !== 'add_link') { - /* remove drag handlers from nodes - */ - nodes.on('mousedown.drag', null).on('touchstart.drag', null); - /* add drag handlers for the add_link tool - */ - nodes.call(drag_add_link); - } else if (new_tool !== 'add_link' && tool === 'add_link') { - /* remove drag handlers for the add_link tool - */ - nodes.on('mousedown.add_link', null).on('mouseup.add_link', null); - /* add drag behavior to nodes - */ - nodes.call(nodeDragging); + } + } + + // main function that draws the topology view + finishSotnView() { + this.graph.model.clear() + this.graph.model.beginUpdate() + this.gLayers = [this.graph.getDefaultParent()] + for (let i = 0; i < 3; i++) { + this.gLayers.push(this.graph.model.root.insert(new mxCell())) + } + + // Insert domains + for (let [networkId, domain] of this.domainMap) { + domain.vertex = this.graph.insertVertex(this.gLayers[0], null, null, 0, 0, 0, 0, this.DOMAIN_STYLE) + domain.vertex.connectable = false + domain.vertex.value = { + label: '(DOMAIN: ' + domain.domainId + ')', + networkId: networkId } - if (new_tool === 'add_node') { - library.show(); - } else { - library.hide(); + // Insert nodes + for (let [nodeId, node] of domain.nodeMap) { + node.vertex = this.graph.insertVertex(domain.vertex, null, null, 0, 0, 26, 26) + node.vertex.value = { + nodeId: nodeId, name: node.name, label: node.name, node: node + } } - return tool = new_tool; - }); - update(); - function update() { - /* update the layout - */ - var links, new_nodes, nodes; - force.nodes(graph.nodes).links(graph.links).start(); - /* create nodes and links - */ - /* (links are drawn with insert to make them appear under the nodes) - */ - /* also define a drag behavior to drag nodes - */ - /* dragged nodes become fixed - */ - nodes = vis.selectAll('.node').data(graph.nodes, function(d) { - return d.id; - }); - new_nodes = nodes.enter().append('g').attr('class', 'node'); -/* .on('click', (function(d) { - /!* SELECTION - *!/ - _this.svcEditorGlobal.selection = d; - d3.selectAll('.node').classed('selected', function(d2) { - return d2 === d; - }); - return d3.selectAll('.link').classed('selected', false); - }));*/ - links = vis.selectAll('.link').data(graph.links, function(d) { - return "" + d.source.id + "->" + d.target.id; - }); - - links.enter().insert('line', '.node').attr('class', 'link').on('click', (function(d) { - /* SELECTION - */ - _this.svcEditorGlobal.selection = d; - d3.selectAll('.link').classed('selected', function(d2) { - return d2 === d; - }); - return d3.selectAll('.node').classed('selected', false); - })); - links - .style("stroke-width", "6px") - .style("stroke", "gray") - .style("opacity", "0.5"); - - links.exit().remove(); - /* TOOLBAR - add link tool initialization for new nodes - */ - if (tool === 'add_link') { - new_nodes.call(drag_add_link); - } else { - new_nodes.call(nodeDragging); + // Insert INNI links + for (let [key, inni] of domain.inniMap) { + inni.edge = this.graph.insertEdge(this.gLayers[1], null, null, + inni.srcNode.vertex, inni.dstNode.vertex, this.LINK_STYLE) + inni.edge.value = { + linkId: key, + inni: inni, + domain: domain + } + //inni.srcNniTp.edge = inni.edge + //inni.dstNniTp.edge = inni.edge } - new_nodes.append('circle').attr('r', 18).attr('stroke', function(d) { - return colorify(d.type); - }).attr('fill', function(d) { - return d3.hcl(colorify(d.type)).brighter(3); - }); - /* draw the label - */ - new_nodes.append('text').text(function(d) { - return d.id; - }).attr('dy', '0.35em').attr('fill', function(d) { - return colorify(d.type); - }); - return nodes.exit().remove(); - }; - function drag_add_link (selection) { - return selection.on('mousedown.add_link', (function(d) { - var p; - new_link_source = d; - /* create the draggable link representation - */ - p = d3.mouse(vis.node()); - drag_link = vis.insert('line', '.node').attr('class', 'drag_link').attr('x1', d.x).attr('y1', d.y).attr('x2', p[0]).attr('y2', p[1]); - drag_link - .style("stroke-width", "6px") - .style("stroke", "gray") - .style("opacity", "0.5"); - /* prevent pan activation - */ - d3.event.stopPropagation(); - /* prevent text selection - */ - return d3.event.preventDefault(); - })).on('mouseup.add_link', (function(d) { - /* add link and update, but only if a link is actually added - */ if (graph.add_link(new_link_source, d) != null) return update(); - })); - }; + // Insert client nodes + for (let [nodeName, clientNode] of domain.clientNodeMap) { + if (clientNode.isCloud) { + clientNode.vertex = this.graph.insertVertex(domain.vertex, null, null, 0, 0, 52, 26, this.CLOUD_STYLE) + } else { + clientNode.vertex = this.graph.insertVertex(domain.vertex, null, null, 0, 0, 26, 13, this.CPE_STYLE) + } + let labelName = nodeName.split('-')[1]; + clientNode.vertex.value = { + name: nodeName, label: labelName, clientNode: clientNode + } + } + // Insert UNI links + for (let [key, uni] of domain.uniMap) { + let srcNode = domain.nodeMap.get(uni.srcNodeId); + uni.edge = this.graph.insertEdge(domain.vertex, null, null, + srcNode.vertex, uni.dstNode.vertex, this.UNI_LINK_STYLE) + uni.edge.value = { + linkId: key, uni: uni, domain: domain, + maximum: 1000000 / 1000000 + 'G', + unused: 1000000 / 1000000 + 'G' + } + //uni.srcUniTp.edge = uni.edge + } + // Insert ENNI links + for (let [key, enni] of this.enniMap) { + let srcNode = null; + let dstNode = null; + + for (let [, tmpDomain] of this.domainMap){ + srcNode = tmpDomain.nodeMap.get(enni.data[0].pnfId); + if (srcNode) break; + } + for (let [, tmpDomain] of this.domainMap){ + dstNode = tmpDomain.nodeMap.get(enni.data[1].pnfId); + if (dstNode) break; + } + + enni.edge = this.graph.insertEdge(this.gLayers[1], null, { + plugId: key, enni: enni }, srcNode.vertex, dstNode.vertex, this.LINK_STYLE) + //enni.data[0].nniTp.edge = enni.edge + //enni.data[1].nniTp.edge = enni.edge + } + } + + // Insert tunnel edges + for (let [id, svc] of this.servicesMap){ + let localTunnelMap = new Map(); + for (let conn of svc.connections){ + let srcArr = conn.srcEpLtpId.split('-'); + let srcNodeId = srcArr[srcArr.length-3]; + let dstArr = conn.dstEpLtpId.split('-'); + let dstNodeId = dstArr[dstArr.length-3]; + if (!localTunnelMap.get(srcNodeId + '-' + dstNodeId)) { + let srcNode = getNode(srcNodeId, this.domainMap); + let dstNode = getNode(dstNodeId, this.domainMap); + if(!srcNode.vertex || !dstNode.vertex) { + continue; + }; + let e2eTunnel = { + name: id + '/' + srcNodeId + '-' + dstNodeId, + srcNode: srcNode, + dstNode: dstNode, + e2eEdge: null, + serviceId: id, + srcVertex: srcNode.vertex, + dstVertex: dstNode.vertex + } + e2eTunnel.e2eEdge = + this.graph.insertEdge(this.gLayers[2], null, { + tunnel: e2eTunnel, tunnelName: e2eTunnel.name + }, e2eTunnel.srcNode.vertex, e2eTunnel.dstNode.vertex, this.TUNNEL_STYLE) + localTunnelMap.set(srcNodeId + '-' + dstNodeId, e2eTunnel) + this.e2eTunnels.push(e2eTunnel); + } + } + } + + + + this.graph.model.endUpdate(); + this.graph.zoomTo(this.graphScale); + this.autoLayout(1); + this.changeLayer({ value:1, name: 'Link Layer'}); + + if (this.serviceSelected){ + this.chooseService(this.serviceSelected); + } + function getNode(nodeId, domainMap){ + let arr = nodeId.split('.'); + return domainMap.get(arr[1]).nodeMap.get(nodeId); + } } - /** - * Redraw the underlay network topology. - * @param {Array} nodes parsed from AAI logicalLinks. - * @param {Array} lines parsed from AAI logicalLinks. - */ - drawTopo(nodes: Array, lines: Array){ - let margin = {top: 20, right: 120, bottom: 20, left: 120}, - width = 1000 - margin.right - margin.left, - height = 350 - margin.top - margin.bottom; + changeLayer(layer){ + this.layerSelected = layer; + for (let i = 1; i < this.gLayers.length; i++) { + this.gLayers[i].setVisible(i == this.layerSelected.value) + } + this.graph.refresh() + } - let thisNg = this; + chooseService(svcInstId) { + this.serviceSelected = svcInstId; + //clear the label and color of all UNI links + for (let [, domain] of this.domainMap) { + for (let [, uni] of domain.uniMap) { + this.graph.setCellStyles(mxConstants.STYLE_STROKECOLOR, CE_COLOR, [uni.edge]) + this.graph.setCellStyles(mxConstants.STYLE_FILLCOLOR, CE_COLOR, [uni.dstNode.vertex]) + this.graph.setCellStyles(mxConstants.STYLE_DASHED, '1', [uni.edge]) + uni.edge.value.label = '' + uni.edge.value.endPoint = null + } + } - let nodeById = d3.map(); - - nodes.forEach(function(node) { - nodeById.set(node["id"], node); - }); - - lines.forEach(function(link) { - link["source"] = nodeById.get(link["source"]); - link["target"] = nodeById.get(link["target"]); - }); - - let svg = d3.select("div#tpContainer").append("svg") - .attr("width", width + margin.right + margin.left) - .attr("height", height + margin.top + margin.bottom) - .style("pointer-events", "all"); - - let graph = svg.append("g").attr("class", "graph"); - - let force = d3.layout.force() - .nodes(nodes) - .links(lines) - .size([width, height]) - /* .linkStrength(function(d){ - switch(d.type){ - case 1: - return 0.15; - case 2: - default: - return 0.1; - } - })*/ - //.gravity(0) - //.gravity(0) - .linkDistance(function (d) { - return 150; - }) - .charge(function(d) { - return -600; - }) - .start(); - - let drag = force.drag() - .on("dragstart", dragstart) - .on("dragend", dragend); - - let _g_lines = graph.selectAll("line.line") - .data(lines) - .enter() - .append("g") - .attr("class", "line"); - - let _g_nodes = graph.selectAll("g.node") - .data(nodes) - .enter() - .append("g") - .attr("class", "node") - .call(drag); - _g_lines.append("line") - .style('stroke', function (d) { - if(d.type === 2){ - return "#000000"; - } else { - return '#93c62d'; - } + // Update the label and color of UNI links of current service + let service = this.servicesMap.get(this.serviceSelected); + if (service){ + let bw = service.bw; + for (let conn of service.connections){ + let srcUniLinkId = getShortName(conn.srcEpLtpId); + let dstUniLinkId = getShortName(conn.dstEpLtpId); + // left side uni link + let arr = srcUniLinkId.split('.'); + let uni = this.domainMap.get(arr[1]).uniMap.get(srcUniLinkId); + colorUni(uni, this.graph, bw); + // right side uni + arr = dstUniLinkId.split('.'); + uni = this.domainMap.get(arr[1]).uniMap.get(dstUniLinkId); + colorUni(uni, this.graph, bw); + + } + } - }) - .style("stroke-width", 4); + // Update tunnel edges + for (let e2eTunnel of this.e2eTunnels) { + e2eTunnel.e2eEdge.setVisible(e2eTunnel.serviceId === svcInstId) + } + this.graph.refresh(); + + // Utility func + function getShortName(ltpId){ + let arr = ltpId.split('-'); + let nodeId = arr[arr.length-3]; + let ltp = arr[arr.length-1]; + return nodeId + '-' + ltp; + } + function colorUni(uni, graph, bw){ + let bandwidth = parseInt(bw); + let newColor = EP_COLOR_MAP.get("update") + graph.setCellStyles(mxConstants.STYLE_STROKECOLOR, newColor, [uni.edge]) + graph.setCellStyles(mxConstants.STYLE_FONTCOLOR, newColor, [uni.edge]) + graph.setCellStyles(mxConstants.STYLE_DASHED, '0', [uni.edge]) + graph.setCellStyles(mxConstants.STYLE_FILLCOLOR, newColor, [uni.dstNode.vertex]) + if (!uni.edge.value.endpoints) { + uni.edge.value.endpoints = []; + } + uni.edge.value.endpoints.push(bandwidth); + let sum = uni.edge.value.endpoints.reduce((a, b) => a + b, 0); + uni.edge.value.label = sum + 'G' ; + } + } - _g_nodes.append("image") - .attr("width", function (d) { - switch (d.group) { - case 'pnf': - return 70; - case 'tp': - default: - return 10; + getValueFromRelationList(rl, relatedTo, relatedKey){ + let rlArr: Array = this.getJsonValue(rl, 'relationship-list.relationship'); + for (let rl of rlArr){ + if (rl['related-to'] === relatedTo){ + let pnfNameS: String; + let tpNameS: String; + for (let rld of rl['relationship-data']){ + if (rld['relationship-key'] === relatedKey){ + let val = rld['relationship-value']; + if (val) return val; + } } - }) - .attr("height", function (d) { - switch (d.group) { - case 'pnf': - return 70; - case 'tp': - default: - return 10; - } - }) - .attr("xlink:href", function (d) { - return thisNg.imgMap[d.group]; - }); + } + } + return null; + } + // get the key of an Endpoint + getEndPointKey(endPoint) { + return endPoint.networkId + '-' + endPoint.nodeId + '-' + endPoint.portId + } - _g_nodes.append("text") - .text(function (d) { - return d.id.substr( d.id.lastIndexOf('-')+1); - }) - .style('font-size', '12') - .style('fill', '#333'); - - //_g_nodes.each(function (d, i) { - var selection = d3.select(this); -/* if (d.status == '0') { - selection.append("g").attr("class", "error-tip") - .append("image").attr("xlink:href", function (d) { - return imgMap['error-tip']; - }); - }*/ - // }); - - _g_lines.each(function (d, i) { - var _this = d3.select(this); - if (d.type === 1) { - _this.append("text") - .text("100GB") - .style('fill', 'rgb(255,198,22)') - .style('font-size', '11'); - - _this.append("rect") - .attr("fill", function (d) { - return '#555'; - }) - .attr("width", function (d) { - return 4; - }) - .attr("height", function (d) { - return 4; - }) - .append("animate"); - - _this.select("rect").append("animate"); - } else { - _this.append("image") - .attr("xlink:href", function () { - return thisNg.imgMap['link-cut']; - }); + // Get color from slice Id + getSliceColor(sliceId) { + // let colorId = sliceMap.get(sliceId) + // if (!sliceId || colorId >= SLICE_COLORS.length) colorId = 1 + return 'black' + } + + // Function to create a table from js array data + createJsonTable(data) { + let table = document.createElement('table') + table.className = 'display compact' + this.tableContainer.nativeElement.appendChild(table) + + if (!data.length) return + let columns = [] + for (let key of Object.keys(data[0])) { + if (this.isBasicType(data[0][key]) && key[0] != '$') { + columns.push({ + data: key, name: key, + title: key.charAt(0).toUpperCase() + key.slice(1), + defaultContent: '' + }) } - }); + } + let dataTable = $(table).DataTable({ + autoWidth: false, + dom: 'ti', + order: [], + select: {toggleable: false}, + columnDefs: [{className: 'dt-center', targets: '_all'}], + columns: columns, + data: data + }) + dataTable.on('user-select', function (e, dt, type, cell, originalEvent) { + if (dt.row('.selected').index() != cell.index().row) + this.selectTableRow(cell.index().row, true) + return false + }) + return dataTable + } - force.on("tick", function (e) { + // disable mouse for application + disableAppMouse(disabled) { + //header.style.pointerEvents = disabled ? 'none' : 'auto' + //container.style.pointerEvents = disabled ? 'none' : 'auto' + this.graph.setEnabled(!disabled) + document.body.style.cursor = disabled ? 'wait' : 'default' + } - _g_lines.select("line").attr("x1", function (d) { - return d.source.x; - }) - .attr("y1", function (d) { - return d.source.y; - }) - .attr("x2", function (d) { - return d.target.x; - }) - .attr("y2", function (d) { - return d.target.y; - }); - _g_lines.select("image").attr("x", function (d) { - var x1 = d.source.x, - x2 = d.target.x, - x = x1 - (x1 - x2) / 2; - return x - 8; - }) - .attr("y", function (d) { - var y1 = d.source.y, - y2 = d.target.y, - y = y1 - (y1 - y2) / 2; - return y - 15; - }); - - _g_lines.select("text") - .attr('x', function (d) { - var x1 = d.source.x, - x2 = d.target.x, - halfX = x1 - (x1 - x2) / 2, - x3 = x1 - (x1 - halfX) / 2; - return x3; - }) - .attr('y', function (d) { - var y1 = d.source.y, - y2 = d.target.y, - halfY = y1 - (y1 - y2) / 2, - y3 = y1 - (y1 - halfY) / 2; - y3 = y3 - 5; - return y3; - }) - .attr("transform", function (d) { - var x1 = d.source.x, - x2 = d.target.x, - y1 = d.source.y, - y2 = d.target.y, - x = x1 - (x1 - x2) / 2, - y = y1 - (y1 - y2) / 2, - rightAngleSide1 = Math.abs(y2 - y1), - rightAngleSide2 = Math.abs(x2 - x1), - _asin = 0, - _rotateAngle = 0, - x3 = x1 - (x1 - x) / 2, - y3 = y1 - (y1 - y) / 2; - - if (x1 < x2) { - _asin = (y2 - y1) / Math.sqrt(Math.pow(rightAngleSide1, 2) + Math.pow( - rightAngleSide2, 2)); - _rotateAngle = Math.asin(_asin) * 180 / Math.PI; - } else { - _asin = (y1 - y2) / Math.sqrt(Math.pow(rightAngleSide1, 2) + Math.pow( - rightAngleSide2, 2)); - _rotateAngle = Math.asin(_asin) * 180 / Math.PI; - _rotateAngle = _rotateAngle < 0 ? (180 + _rotateAngle) : -(180 - - _rotateAngle); - } - return 'rotate(' + (_rotateAngle) + ',' + x3 + ' ' + y3 + ')'; - }); + choseConnectivity(item) { + if (this.connectivitySelected !== item) this.connectivitySelected = item; + //this.drawService(this.getSvcTree()); + } - _g_lines.select("rect") - .attr('x', function (d) { - return d.source.x - 1; - }) - .attr('y', function (d) { - return d.source.y - 1; - }) - .selectAll('animate').each(function (d, i) { - if (i == 0) { - d3.select(this) - .attr("attributeName", function (d) { - return 'x'; - }) - .attr("from", function (d) { - return d.source.x - 1; - }) - .attr("to", function (d) { - return d.target.x; - }); - } else { - d3.select(this) - .attr("attributeName", function (d) { - return 'y'; - }) - .attr("from", function (d) { - return d.source.y - 1; - }) - .attr("to", function (d) { - return d.target.y; - }); - } + _debounce(func: Function, delay: number) { + let inDebounce; + return function () { + const context = this; + const args = arguments; + clearTimeout(inDebounce) + inDebounce = setTimeout(() => func.apply(context, args), delay); + } + } - d3.select(this).attr("attributeType", "XML") - .attr("dur", function (d) { - return '1.5s'; - }) - .attr("repeatCount", "indefinite"); + // Layout the label of client nodes + clientNodeLabelLayout() { + for (let [,sotnDomain] of this.domainMap) { + for (let [, clientNode] of sotnDomain.clientNodeMap) { + let vertex = clientNode.vertex + let remote = clientNode.networkNode.vertex + let isAbove = (vertex.geometry.y + vertex.geometry.height / 2) < (remote.geometry.y + remote.geometry.height / 2) + this.graph.setCellStyles(mxConstants.STYLE_VERTICAL_LABEL_POSITION, isAbove ? 'top' : 'bottom', [vertex]) + this.graph.setCellStyles(mxConstants.STYLE_VERTICAL_ALIGN, isAbove ? 'bottom' : 'top', [vertex]) + } + } + } - }) -/* let k = 6 * e.alpha; - nodes.forEach(function(o, i) { - if (o["layer"] === "Otn"){ - o["y"] += k - //o["x"] += i & 2 ? k : -k; - - } else if (o["layer"] === "Eth") { - o["y"] += -k; - //o["x"] += i & 2 ? k : -k; - } - });*/ + // Center the graph in the container + centerGraph() { + + let domains = this.graph.model.getChildVertices(this.gLayers[0]) + if (!domains) return + let bounds = this.graph.getBoundingBoxFromGeometry(domains, false) + if (!bounds) return + this.graphScale = this.graph.view.scale + let dx = (this.graph.container.clientWidth / this.graphScale - bounds.width) / 2 + let dy = (this.graph.container.clientHeight / this.graphScale - bounds.height) / 2 + this.graph.view.setTranslate(dx < 0 ? 0 : dx, dy < 0 ? 0 : dy) + this.graph.refresh() + } - _g_nodes.attr("transform", function (d) { - if(d.group === 'pnf') { - var image = d3.select(this).select("image")[0][0], - halfWidth = parseFloat("70") / 2, - halfHeight = parseFloat("70") / 2; + // Functions ..0. + // Create popup menu for graph + createPopupMenu(menu, cell, evt) { + } - return 'translate(' + (d.x - halfWidth) + ',' + (d.y - halfHeight) + ')'; - } else { - return 'translate(' + (d.x) + ',' + (d.y) + ')'; + // A utility function for setting the values of part of an Object's properties + setObjValues(obj, newValues, keyObj = null): void { + for (let key of Object.keys(newValues)) { + if (!keyObj) obj[key] = newValues[key] + else obj[keyObj[key]] = newValues[key] + } + } + + // Save changed services to local storage + saveServices() { + let storedServices = [] + for (let e2eService of this.e2eServices) { + let storedService = this.cloneWithSimpleProperties(e2eService) + storedService["endPoints"] = [] + for (let endPointMap of [e2eService.rootEndPointMap, e2eService.leafEndPointMap]) { + for (let [, endPoint] of endPointMap) { + if (endPoint.status == 'retrieve') continue + storedService["endPoints"].push(this.cloneWithSimpleProperties(endPoint)) } + } + storedServices.push(storedService) + } + (this.storage)['actn-viewer-service'] = JSON.stringify(storedServices) + } - }); + // Hold for next release + // Read stored services from local storage. + readServices() { + /* let storedServices = JSON.parse((this.storage)['actn-viewer-service'] || '[]') + for (let storedService of storedServices) { + let e2eService = this.e2eServiceMap.get(storedService.name) + if (!e2eService) { //not exist then add new one + e2eService = storedService + e2eService.leafEndPointMap = new Map() + e2eService.rootEndPointMap = new Map() + e2eService.active = false + for (let endPoint of e2eService.endPoints) { + if (!endPoint.newBand) endPoint.newBand = endPoint.bandwidth + this.addNewEndPointToService(e2eService, endPoint, 'create') + } + this.updateOneE2eService(e2eService) + } else { //otherwise update the status of endpoint + for (let storedEndPoint of storedService.endPoints) { + let endPoint = e2eService.leafEndPointMap.get(storedEndPoint.id) || e2eService.rootEndPointMap.get(storedEndPoint.id) + if (endPoint) { + switch (storedEndPoint.status) { + case 'create': + break + case 'delete': + endPoint.status = 'delete'; + endPoint.newBand = 0; + break + case 'update': + if (endPoint.bandwidth != storedEndPoint.newBand) { + endPoint.newBand = storedEndPoint.newBand + endPoint.status = 'update' + } + } + } else { + this.addNewEndPointToService(e2eService, storedEndPoint, 'create') + } + } + this.updateRootBandwidth(e2eService) + } + } */ + } - _g_nodes.select("text").attr('dy', function (d) { - var image = this.previousSibling, - height = parseFloat("10"), - fontSize = parseFloat(this.style.fontSize); + // add a new endpoint to e2e service + addNewEndPointToService(e2eService, endPoint, status) { + endPoint.edge = this.getUniEdgeFromEp(endPoint) + if (!endPoint.edge) return + endPoint.status = status + let endPointMap = endPoint.role == 'leaf-access' ? e2eService.leafEndPointMap : e2eService.rootEndPointMap + endPoint.id = endPoint.id || this.getEndPointKey(endPoint) + endPointMap.set(endPoint.id, endPoint) + } - return height + 1.5 * fontSize; - }); + // add a new uni to e2e service + addNewUni(e2eService, uni, isLeaf) { + let newEndPoint = { + networkId: uni.networkId, + nodeId: uni.srcNode.nodeId, + portId: uni.srcUniTp.tpId, + role: isLeaf ? 'leaf-access' : 'root-primary', + newBand: this.defBandwidth, + } + this.addNewEndPointToService(e2eService, newEndPoint, 'create') + this.updateCurrentCloud(e2eService) + } - _g_nodes.select(".error-tip").attr("transform", function (d) { + deleteNewUni(e2eService, endPoint, isLeaf, doUpdate = true) { + let endPointMap = isLeaf ? e2eService.leafEndPointMap : e2eService.rootEndPointMap + endPointMap.delete(endPoint.id) + if (doUpdate) this.updateCurrentCloud(e2eService) + } - var image = this.parentNode.firstChild, - width = parseFloat("70"); + deleteOldUni(e2eService, endPoint) { + endPoint.status = 'delete' + endPoint.newBand = 0 + this.updateCurrentCloud(e2eService) + } - return 'translate(' + 0.8 * width + ',0)'; + cancelUniDeletion(e2eService, endPoint, doUpdate = true) { + endPoint.status = 'retrieve' + endPoint.newBand = endPoint.bandwidth + if (doUpdate) this.updateCurrentCloud(e2eService) + } - }); + cancelUniModification(e2eService, endPoint, doUpdate = true) { + endPoint.status = 'retrieve' + endPoint.newBand = endPoint.bandwidth + if (doUpdate) this.updateCurrentCloud(e2eService) + } - }); + updateCurrentCloud(e2eService) { + this.updateRootBandwidth(e2eService) + this.saveServices() + //this.showSotnCloud(this.currentCloud) + } + // Calculate and update the bandwidth of roots + updateRootBandwidth(e2eService) { + let newRootBand = 0 + for (let [, endPoint] of e2eService.leafEndPointMap) { + newRootBand += endPoint.newBand + } + for (let [, endPoint] of e2eService.rootEndPointMap) { + if (endPoint.status == 'delete') continue + endPoint.newBand = newRootBand + if (endPoint.status == 'create') continue + endPoint.status = (endPoint.newBand != endPoint.bandwidth) ? 'update' : 'retrieve' + } + e2eService.changed = false + for (let endPointMap of [e2eService.leafEndPointMap, e2eService.rootEndPointMap]) { + for (let [, endPoint] of endPointMap) { + if (endPoint.status != 'retrieve') { + e2eService.changed = true; + break + } + } + if (e2eService.changed) break + } + this.serviceTable.row(e2eService.$index).data(e2eService) + } - function dblclick(d) { - d3.select(this).classed("fixed", d.fixed = false); + // cancel all service changes + cancelAllCloudChanges(currentService) { + for (let endPointMap of [currentService.leafEndPointMap, currentService.rootEndPointMap]) { + for (let [, endPoint] of endPointMap) { + switch (endPoint.status) { + case 'create': + this.deleteNewUni(currentService, endPoint, endPoint.role == 'leaf-access', false) + break + case 'delete': + this.cancelUniDeletion(currentService, endPoint, false) + break + case 'update': + if (endPoint.role == 'leaf-access') { + this.cancelUniModification(currentService, endPoint, false) + } + break + } + } } + currentService.changed = false + this.updateCurrentCloud(currentService) + } - function dragstart(d) { - d3.select(this).classed("fixed", d.fixed = true); - thisNg.eventDispatcher.dispatch(new AppEvent(AppEventType.UserNodeDrag, d)); + // function for interact with ONAP SO + onapApplyCloudChanges(currentService) { + alert('Future feature.') + // let newSliceServices = e2eServices.filter(service => + // service.sliceId == currentSlice && !service.active) + // if (newSliceServices.length == 0) return + // let connections = [] + // newSliceServices.forEach((s, i) => { + // connections.push({ + // epa: s.srcVertex.value.node.uniTpMap.get(s.srcPort).name.split(':')[0], + // epb: s.dstVertex.value.node.uniTpMap.get(s.dstPort).name.split(':')[0], + // bandwidth: s.bandwidth, + // name: s.name, + // }) + // }) + // let jsonData = jsonRender('ONAP_TS_ALLOCATE_TMPL', + // { sliceId: currentSlice, connections: connections }) + // setRestJsonData('POST', onap, 'allocate', jsonData, null, false) + + // let jsonData = jsonRender('ONAP_TS_OTHERS_TMPL', + // { sliceId: currentSlice }) + // setRestJsonData(method, onap, action, jsonData, null, false) + } + + // Reset the database of controllers +/* resetControllerData() { + let result = confirm('The data of all controllers will be reset to initial state.') + if (!result) return + let reqNumber = 0 + for (let controller of controllers) { + setRestJsonData('POST', {controller: controller}, RPC_RESET_DATA_URL, null) } - function dragend(d) { + }*/ + // Filter table by cloud Id + filterTable(table, cloudId) { + if (table) table.column('cloudId:name').search(cloudId, true).draw() + } + + // Hide or show table + hideTable(table, hide) { + let index; + if (table && !hide) { + $(table.table().container()).show() + index = table.row({selected: true, search: 'applied'}).index() + if (index == null) { + this.tableContainer.nativeElement.scrollTo(0, 0) + } else { + table.row(index).node().scrollIntoView(false) + } + } else if (table && hide) { + $(table.table().container()).hide() } + } - function color (d){ - const scale = d3.scaleOrdinal(d3.schemeCategory10); - switch(d.group){ - case "pnf": - return scale(1); - case "tp": - return scale(2); - default: - return scale(9); + // Select table row + selectTableRow(index = null, click = false) { + this.graph.clearSelection() + if ((this.currentLayer == 1 || this.currentLayer == 2) && this.tunnelTable) { + let indexes + if (index == null) { + indexes = this.tunnelTable.rows({selected: true, search: 'applied'}).indexes() + if (indexes.length == 0) return this.tunnelTable.rows('.selected').deselect() + indexes = indexes.toArray() + } else { + this.tunnelTable.rows('.selected').deselect() + let node = this.tunnelTable.row(index).select().node() + if (!click) node.scrollIntoView(false) + this.serviceTable.rows('.selected').deselect() + indexes = [index] } + for (let i in indexes) { + mxConstants.EDGE_SELECTION_COLOR = parseInt(i) % 2 ? '#0000FF' : '#00FF00' + this.graph.addSelectionCells((this.currentLayer == 1) ? + this.e2eTunnels[indexes[i]].edges : [this.e2eTunnels[indexes[i]].e2eEdge]) + this.showJsonData(this.tunnelsMap.get(this.e2eTunnels[indexes[i]].name), false) + } + mxConstants.EDGE_SELECTION_COLOR = '#00FF00' + } else if (this.currentLayer == 3 && this.serviceTable) { + if (index == null) { + index = this.serviceTable.row({selected: true, search: 'applied'}).index() + if (index == null) return this.serviceTable.rows('.selected').deselect() + } else { + this.serviceTable.rows('.selected').deselect() + let node = this.serviceTable.row(index).select().node() + if (!click) node.scrollIntoView(false) + } + this.graph.addSelectionCell(this.e2eServices[index].e2eEdge) + let currentService = this.e2eServices[index].name + this.showJsonData(this.servicesMap.get(this.e2eServices[index].name), false) + if (this.tunnelTable) this.tunnelTable.rows('.selected').deselect() } } - choseConnectivity(item) { - if (this.connectivitySelected !== item) this.connectivitySelected = item; - this.drawService(this.getSvcTree()); + // Function to show the data of js object in JSON format + showJsonData(data, show = true) { + /* $(jsonViewer).jsonViewer(data, { withLinks: false }) + if (show) { popupWnd.show(); popupWnd.fit() }*/ } - getSvcTree(): Array { - let tree = [] - let rel = this.connectivitySelected["relationship-list"]["relationship"] || null; - if (rel){ - tree = rel.filter(rl => rl["related-to"] === "uni") - .map(obj => { - let rObj ={}; - rObj["id"] = obj["relationship-data"][0]["relationship-value"], - rObj["type"] = "leaf"; - return rObj; - }) +// A utility function for getting value from nested JSON data + getJsonValue(obj, path) { + if (!obj) return + let arr = path.split('.'); + let tempObj = obj; + for (let e of arr) { + if (!e) continue + if (!(tempObj = tempObj[e])) + return; + } + return tempObj; + } + +// A utility function for getting the last part of a string split by : + getLastColonPart(longStr) { + if (!longStr) return '' + let strArray = longStr.split(':') + if (strArray.length > 1) return strArray[strArray.length - 1] + else return '' + } + +// A utility function for cloning a new object with simple properties of the source ojbect + cloneWithSimpleProperties(source) { + let target = {} + for (let key of Object.keys(source)) { + if (this.isBasicType(source[key])) { + target[key] = source[key] } - return tree; + } + return target + } + +// A utility function for copying same properties from the source to the target + copySameProperties(source, target) { + for (let key of Object.keys(source)) { + if (key in target) target[key] = source[key] + } + } + +// A utility function for getting a JSON Array + getJsonArray(obj, path) { + let tmpObj = this.getJsonValue(obj, path) + if (Array.isArray(tmpObj)) return tmpObj + else return + } + +// basic type of js object + isBasicType(obj) { + return /^(string|number|boolean)$/.test(typeof obj) + } + + + // Get uni edge from endpoint + getUniEdgeFromEp(ep) { + let node = this.getNodeFromId(ep.networkId, ep.nodeId) + if (!node || !ep.portId) return null + let uniTp = node.uniTpMap.get(ep.portId.toString()) + if (uniTp) return uniTp.edge + } + + + // Get node from Ids + getNodeFromId(networkId, nodeId) { + if (!networkId || !nodeId) return + let domain = this.domainMap.get(networkId) + if (!domain) return + return domain.nodeMap.get(nodeId) } - getNodes(ptMapping: Array) : Array{ - let nodes = []; - for (let pnf of ptMapping){ - if (pnf["layer"] === 2){ - continue; + // Lay out the multi-domain topology automatically + autoLayout(layer = null) { + switch (layer || this.currentLayer) { + case 1: this.vertexLayout(); + case 2: this.edgeLayout.execute((this.gLayers)[2]); break + case 3: this.edgeLayout.execute((this.gLayers)[3]); break + } + } + // Lay out domains and nodes automatically + vertexLayout() { + // Move nodes out of domain for layout + for (let [, sotnDomain] of this.domainMap) { + for (let [, node] of sotnDomain.nodeMap) { + this.graph.model.add((this.gLayers)[1], node.vertex) } - let name = pnf["pnfName"]; - let newNode = { - "id" : name, - "group": "pnf", - "radius" : 2, - "layer" : pnf["layer"] === 2? "Eth" : "Otn" + for (let [, clientNode] of sotnDomain.clientNodeMap) { + this.graph.model.add((this.gLayers)[1], clientNode.vertex) } - nodes.push(newNode); } - return nodes; - } + // Disconnect tunnel edge from source and target + for (let item of this.e2eTunnels) { + item.srcVertex.removeEdge(item.e2eEdge, true) + item.dstVertex.removeEdge(item.e2eEdge, false) + } - getLinks(logicalLinks: Array, ptMapping: Array) : Array { - let links = []; - for (let ll of logicalLinks){ - let lkName:string = ll["link-name"]; - let topoIdIdx:number = lkName.lastIndexOf("topologyId-"); - if (topoIdIdx !== -1 && lkName.charAt(topoIdIdx + 11) === '2'){ - //Ignore - continue; - } else if (typeof ll["relationship-list"] === 'undefined' || - typeof ll["relationship-list"]["relationship"] === 'undefined'){ - continue; + // Lay out the muti-domain topology including all nodes + this.graph.zoomTo(1); + this.organicLayout.execute(this.gLayers[1]) + // get the center point of each domain + let centerPoints = [] + for (let [, sotnDomain] of this.domainMap) { + let x = 0, y = 0 + if (sotnDomain.nodeMap.size == 0) continue + for (let [, node] of sotnDomain.nodeMap) { + x += node.vertex.geometry.x + y += node.vertex.geometry.y } - //pnf to pnf - let endpoints = []; - for (let pi of ll["relationship-list"]["relationship"]) { - if (pi["related-to"] === "p-interface"){ - for (let rd of pi["relationship-data"]){ - if (rd["relationship-key"] === "pnf.pnf-name"){ - endpoints.push(rd["relationship-value"]); - } - } + centerPoints.push({ x: x / sotnDomain.nodeMap.size, y: y / sotnDomain.nodeMap.size }) + } + // calculate the rotation angel of topology in order to rotote it to horizontal direction + if (centerPoints.length >= 2) { + let theta = Math.atan2(centerPoints[centerPoints.length - 1].y - centerPoints[0].y, + centerPoints[centerPoints.length - 1].x - centerPoints[0].x) * 180 / Math.PI + let i = 0 + for (let [, sotnDomain] of this.domainMap) { + for (let [, node] of sotnDomain.nodeMap) { + rotateVertex(node.vertex, centerPoints[i], -theta) } - } - if (endpoints.length === 2){ - let newlk = { - "source": endpoints[0], - "target": endpoints[1], - "type" : 1 + for (let [, clientNode] of sotnDomain.clientNodeMap) { + let vertex = clientNode.vertex + rotateVertex(vertex, centerPoints[i], -theta) + // shorten the length of uni link by 1/3 + let remote = clientNode.networkNode.vertex + vertex.geometry.x = (vertex.geometry.x * 2 + remote.geometry.x) / 3 + vertex.geometry.y = (vertex.geometry.y * 2 + remote.geometry.y) / 3 } - links.push(newlk); + i++ } } - return links; - } - - getPnfTpMapping(logicalLinks: Array) { - let pnfs = []; - let pnfVisited = {}; - let pnfIndex: number = 0; - for (let ll of logicalLinks){ - let lkName:string = ll["link-name"]; - let topoIdIdx:number = lkName.lastIndexOf("topologyId-"); - if (topoIdIdx !== -1 && lkName.charAt(topoIdIdx + 11) === '2'){ - //Ethernet layer logical-link - let lastDashIdx:number = lkName.lastIndexOf("-"); - let pnfName: string = lkName.replace("linkId", "nodeId").substr(0, lastDashIdx); - let uniName: string = lkName.substr( lastDashIdx+1); - - if (pnfVisited[pnfName]){ - let idx: number = parseInt(pnfVisited[pnfName].substr(1)); - pnfs[idx].tps[uniName] = true; - } else { - pnfVisited[pnfName] = '#' + pnfIndex; - let newPnf = { - "pnfName" : pnfName, - "tps" : { - }, - "layer" :2 - } - newPnf.tps[uniName] = true; - pnfs.push(newPnf); - pnfIndex++; - - } - continue; - } else if (ll["relationship-list"] === undefined || - ll["relationship-list"]["relationship"].length === 0 ){ - continue; + // resize the domains to just contain the nodes in each domain + for (let [, sotnDomain] of this.domainMap) { + let vertexes = [] + for (let [, node] of sotnDomain.nodeMap) { + vertexes.push(node.vertex) } - for (let pi of ll["relationship-list"]["relationship"]) { - if (pi["related-to"] === "p-interface"){ - let pnfName:string; - let tpName:string; - for (let rd of pi["relationship-data"]){ - if (rd["relationship-key"] === "pnf.pnf-name"){ - pnfName = rd["relationship-value"]; - } else if (rd["relationship-key"] === "p-interface.interface-name"){ - tpName = rd["relationship-value"]; - } - } - if (pnfVisited[pnfName]){ - let idx: number = parseInt(pnfVisited[pnfName].substr(1)); - pnfs[idx].tps[tpName] = true; - } else { - pnfVisited[pnfName] = '#' + pnfIndex; - let newPnf = { - "pnfName" : pnfName, - "tps" : { - }, - "layer" : 1 - } - newPnf.tps[tpName] = true; - pnfs.push(newPnf); - pnfIndex++; + for (let [, clientNode] of sotnDomain.clientNodeMap) { + vertexes.push(clientNode.vertex) + } + let bounds = this.graph.getBoundingBoxFromGeometry(vertexes, false) + sotnDomain.vertex.geometry.setRect(bounds.x, bounds.y, bounds.width, bounds.height) + } + // Move nodes back to their orginal domain + for (let [, sotnDomain] of this.domainMap) { + for (let [, node] of sotnDomain.nodeMap) { + this.graph.model.add(sotnDomain.vertex, node.vertex) + } + for (let [, clientNode] of sotnDomain.clientNodeMap) { + this.graph.model.add(sotnDomain.vertex, clientNode.vertex) + } + } + // Reconnect tunnel edge to source and target + for (let item of this.e2eTunnels) { + item.srcVertex.insertEdge(item.e2eEdge, true) + item.dstVertex.insertEdge(item.e2eEdge, false) + } - } - } + this.clientNodeLabelLayout() + this.domainLayout(50, 50, 50) + this.centerGraph() + + function rotateVertex(vertex, centerPoint, theta) { + let newPoint = mxAbstractCanvas2D.prototype.rotatePoint( + vertex.geometry.x, vertex.geometry.y, theta, centerPoint.x, centerPoint.y) + vertex.geometry.x = newPoint.x + vertex.geometry.y = newPoint.y + } + } + // Layout domains automatically with the same domain size + domainLayout(marginWidth, marginHeight, marginBetween) { + let domains = this.graph.model.getChildVertices(this.gLayers[0]) + this.graph.cellsFolded(domains, false, false) + let maxWidth = 0, maxHeight = 0 + for (let domain of domains) { + if (maxWidth < domain.geometry.width) maxWidth = domain.geometry.width + if (maxHeight < domain.geometry.height) maxHeight = domain.geometry.height + } + maxWidth += marginWidth; + maxHeight += marginHeight + // Center the nodes in the domain + for (let domain of domains) { + domain.geometry.width = maxWidth + marginBetween + domain.geometry.height = maxHeight + let nodes = this.graph.model.getChildVertices(domain) + let bounds = this.graph.getBoundingBoxFromGeometry(nodes, false) + let xOffset = (maxWidth / 2 - bounds.x - bounds.width / 2) + let yOffset = (maxHeight / 2 - bounds.y - bounds.height / 2) + for (let node of nodes) { + node.geometry.x += xOffset + node.geometry.y += yOffset } } - return pnfs; + this.stackLayout.execute(this.gLayers[0]) + for (let domain of domains) { + domain.geometry.width -= marginBetween + } } -} + getKeys(map){ + return Array.from(map.keys()); + } + getValues(map){ + return Array.from(map.values()); + } + +} \ No newline at end of file -- cgit 1.2.3-korg