summaryrefslogtreecommitdiffstats
path: root/catalog-ui/app/scripts/directives/graphs-v2
diff options
context:
space:
mode:
Diffstat (limited to 'catalog-ui/app/scripts/directives/graphs-v2')
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/common/common-graph-utils.ts361
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/common/style/component-instances-nodes-style.ts259
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/common/style/module-node-style.ts92
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.directive.ts555
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.html22
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.less14
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-general-utils.ts243
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-links-utils.ts347
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-nodes-utils.ts220
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/match-capability-requierment-utils.ts265
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.directive.ts114
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.html2
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.less14
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-utils/deployment-graph-general-utils.ts24
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/image-creator/image-creator.service.ts46
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/palette/interfaces/i-dragdrop-event.d.ts7
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/palette/palette.directive.ts327
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/palette/palette.html59
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/palette/palette.less92
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.html63
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.less118
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.ts113
22 files changed, 3357 insertions, 0 deletions
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/common/common-graph-utils.ts b/catalog-ui/app/scripts/directives/graphs-v2/common/common-graph-utils.ts
new file mode 100644
index 0000000000..e01e455e93
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/common/common-graph-utils.ts
@@ -0,0 +1,361 @@
+/**
+ * Created by obarda on 12/21/2016.
+ */
+/**
+ * Created by obarda on 12/13/2016.
+ */
+/// <reference path="../../../references"/>
+module Sdc.Graph.Utils {
+
+ export class CommonGraphUtils {
+
+ constructor(private NodesFactory:Sdc.Utils.NodesFactory, private LinksFactory:Sdc.Utils.LinksFactory) {
+
+ }
+
+ public safeApply = (scope:ng.IScope, fn:any) => { //todo remove to general utils
+ let phase = scope.$root.$$phase;
+ if (phase == '$apply' || phase == '$digest') {
+ if (fn && (typeof(fn) === 'function')) {
+ fn();
+ }
+ } else {
+ scope.$apply(fn);
+ }
+ };
+
+ /**
+ * Draw node on the graph
+ * @param cy
+ * @param compositionGraphNode
+ * @param position
+ * @returns {CollectionElements}
+ */
+ public addNodeToGraph(cy:Cy.Instance, compositionGraphNode:Models.Graph.CommonNodeBase, position?:Cy.Position):Cy.CollectionElements {
+
+ var node = cy.add(<Cy.ElementDefinition> {
+ group: 'nodes',
+ position: position,
+ data: compositionGraphNode,
+ classes: compositionGraphNode.classes
+ });
+
+ if(!node.data().isUcpe) { //ucpe should not have tooltip
+ this.initNodeTooltip(node);
+ }
+ return node;
+ };
+
+ /**
+ * The function will create a component instance node by the componentInstance position.
+ * If the node is UCPE the function will create all cp lan&wan for the ucpe
+ * @param cy
+ * @param compositionGraphNode
+ * @returns {Cy.CollectionElements}
+ */
+ public addComponentInstanceNodeToGraph(cy:Cy.Instance, compositionGraphNode:Models.Graph.CompositionCiNodeBase):Cy.CollectionElements {
+
+ let nodePosition = {
+ x: +compositionGraphNode.componentInstance.posX,
+ y: +compositionGraphNode.componentInstance.posY
+ };
+
+ let node = this.addNodeToGraph(cy, compositionGraphNode, nodePosition);
+ if (compositionGraphNode.isUcpe) {
+ this.createUcpeCpNodes(cy, node);
+ }
+ return node;
+ };
+
+ /**
+ * This function will create CP_WAN & CP_LAN for the UCPE. this is a special node on the group that will behave like ports on the ucpe
+ * @param cy
+ * @param ucpeGraphNode
+ */
+ private createUcpeCpNodes(cy:Cy.Instance, ucpeGraphNode:Cy.CollectionNodes):void {
+
+ let requirementsArray:Array<any> = ucpeGraphNode.data().componentInstance.requirements["tosca.capabilities.Node"];
+ //show only LAN or WAN requirements
+ requirementsArray = _.reject(requirementsArray, (requirement:any) => {
+ let name:string = requirement.ownerName.toLowerCase();
+ return name.indexOf('lan') === -1 && name.indexOf('wan') === -1;
+ });
+ requirementsArray.sort(function (a, b) {
+ let nameA = a.ownerName.toLowerCase().match(/[^ ]+/)[0];
+ let nameB = b.ownerName.toLowerCase().match(/[^ ]+/)[0];
+ let numA = _.last(a.ownerName.toLowerCase().split(' '));
+ let numB = _.last(b.ownerName.toLowerCase().split(' '));
+
+ if (nameA === nameB) return numA > numB ? 1 : -1;
+ return nameA < nameB ? 1 : -1;
+ });
+ let position = angular.copy(ucpeGraphNode.boundingbox());
+ //add CP nodes to group
+ let topCps:number = 0;
+ for (let i = 0; i < requirementsArray.length; i++) {
+
+ let cpNode = this.NodesFactory.createUcpeCpNode(angular.copy(ucpeGraphNode.data().componentInstance));
+ cpNode.componentInstance.capabilities = requirementsArray[i];
+ cpNode.id = requirementsArray[i].ownerId;
+ cpNode.group = ucpeGraphNode.data().componentInstance.uniqueId;
+ cpNode.name = requirementsArray[i].ownerName; //for tooltip
+ cpNode.displayName = requirementsArray[i].ownerName;
+ cpNode.displayName = cpNode.displayName.length > 5 ? cpNode.displayName.substring(0, 5) + '...' : cpNode.displayName;
+
+
+ if (cpNode.name.toLowerCase().indexOf('lan') > -1) {
+ cpNode.textPosition = "top";
+ cpNode.componentInstance.posX = position.x1 + (i * 90) - (topCps * 90) + 53;
+ cpNode.componentInstance.posY = position.y1 + 400 + 27;
+ } else {
+ cpNode.textPosition = "bottom";
+ cpNode.componentInstance.posX = position.x1 + (topCps * 90) + 53;
+ cpNode.componentInstance.posY = position.y1 + 27;
+ topCps++;
+ }
+ let cyCpNode = this.addComponentInstanceNodeToGraph(cy, cpNode);
+ cyCpNode.lock();
+ }
+ };
+
+ /**
+ *
+ * @param nodes - all nodes in graph in order to find the edge connecting the two nodes
+ * @param fromNodeId
+ * @param toNodeId
+ * @returns {boolean} true/false if the edge is certified (from node and to node are certified)
+ */
+ public isRelationCertified(nodes:Cy.CollectionNodes, fromNodeId:string, toNodeId:string):boolean {
+ let resourceTemp = _.filter(nodes, function (node:Cy.CollectionFirst) {
+ return node.data().id === fromNodeId || node.data().id === toNodeId;
+ });
+ let certified:boolean = true;
+
+ _.forEach(resourceTemp, (item) => {
+ certified = certified && item.data().certified;
+ });
+
+ return certified;
+ }
+
+ /**
+ * Add link to graph - only draw the link
+ * @param cy
+ * @param link
+ */
+ public insertLinkToGraph = (cy:Cy.Instance, link:Models.CompositionCiLinkBase) => {
+
+ if (!this.isRelationCertified(cy.nodes(), link.source, link.target)) {
+ link.classes = 'not-certified-link';
+ }
+ cy.add({
+ group: 'edges',
+ data: link,
+ classes: link.classes
+ });
+
+ };
+
+ /**
+ * go over the relations and draw links on the graph
+ * @param cy
+ * @param instancesRelations
+ */
+ public initGraphLinks(cy:Cy.Instance, instancesRelations:Array<Models.RelationshipModel>) {
+
+ if (instancesRelations) {
+ _.forEach(instancesRelations, (relationshipModel:Models.RelationshipModel) => {
+ _.forEach(relationshipModel.relationships, (relationship:Models.Relationship) => {
+ let linkToCreate = this.LinksFactory.createGraphLink(cy, relationshipModel, relationship);
+ this.insertLinkToGraph(cy, linkToCreate);
+ });
+ });
+ }
+ }
+
+ /**
+ * Determine which nodes are in the UCPE and set child data for them.
+ * @param cy
+ */
+ public initUcpeChildren(cy:Cy.Instance){
+ let ucpe:Cy.CollectionNodes = cy.nodes('[?isUcpe]'); // Get ucpe on graph if exist
+ _.each(cy.edges('.ucpe-host-link'), (link)=>{
+
+ let ucpeChild:Cy.CollectionNodes = (link.source().id() == ucpe.id())? link.target() : link.source();
+ this.initUcpeChildData(ucpeChild, ucpe);
+
+ //vls dont have ucpe-host-link connection, so need to find them and iterate separately
+ let connectedVLs = ucpeChild.connectedEdges().connectedNodes('.vl-node');
+ _.forEach(connectedVLs, (vl)=>{ //all connected vls must be UCPE children because not allowed to connect to a VL outside of the UCPE
+ this.initUcpeChildData(vl, ucpe);
+ });
+ });
+ }
+
+ /**
+ * Set properties for nodes contained by the UCPE
+ * @param childNode- node contained in UCPE
+ * @param ucpe- ucpe container node
+ */
+ public initUcpeChildData(childNode:Cy.CollectionNodes, ucpe:Cy.CollectionNodes){
+
+ if(!childNode.data('isInsideGroup')){
+ this.updateUcpeChildPosition(childNode, ucpe);
+ childNode.data({isInsideGroup: true});
+ }
+
+ }
+
+ /**
+ * Updates UCPE child node offset, which allows child nodes to be dragged in synchronization with ucpe
+ * @param childNode- node contained in UCPE
+ * @param ucpe- ucpe container node
+ */
+ public updateUcpeChildPosition(childNode:Cy.CollectionNodes, ucpe:Cy.CollectionNodes){
+ let childPos:Cy.Position = childNode.relativePosition();
+ let ucpePos:Cy.Position = ucpe.relativePosition();
+ let offset:Cy.Position = {
+ x: childPos.x - ucpePos.x,
+ y: childPos.y - ucpePos.y
+ };
+ childNode.data("ucpeOffset", offset);
+ }
+
+ /**
+ * Removes ucpe-child properties from the node
+ * @param childNode- node being removed from UCPE
+ */
+ public removeUcpeChildData(childNode:Cy.CollectionNodes){
+ childNode.removeData("ucpeOffset");
+ childNode.data({isInsideGroup: false});
+
+ }
+
+
+ public HTMLCoordsToCytoscapeCoords(cytoscapeBoundingBox:Cy.Extent, mousePos:Cy.Position):Cy.Position {
+ return {x: mousePos.x + cytoscapeBoundingBox.x1, y: mousePos.y + cytoscapeBoundingBox.y1}
+ };
+
+
+ public getCytoscapeNodePosition = (cy: Cy.Instance, event:IDragDropEvent):Cy.Position => {
+ let targetOffset = $(event.target).offset();
+ let x = event.pageX - targetOffset.left;
+ let y = event.pageY - targetOffset.top;
+
+ return this.HTMLCoordsToCytoscapeCoords(cy.extent(), {
+ x: x,
+ y: y
+ });
+ };
+
+
+ public getNodePosition(node:Cy.CollectionFirstNode):Cy.Position{
+ let nodePosition = node.relativePoint();
+ if(node.data().isUcpe){ //UCPEs use bounding box and not relative point.
+ nodePosition = {x: node.boundingbox().x1, y: node.boundingbox().y1};
+ }
+
+ return nodePosition;
+ }
+
+ /**
+ * return true/false if first node contains in second - this used in order to verify is node is entirely inside ucpe
+ * @param firstBox
+ * @param secondBox
+ * @returns {boolean}
+ */
+ public isFirstBoxContainsInSecondBox(firstBox:Cy.BoundingBox, secondBox:Cy.BoundingBox) {
+
+ return firstBox.x1 > secondBox.x1 && firstBox.x2 < secondBox.x2 && firstBox.y1 > secondBox.y1 && firstBox.y2 < secondBox.y2;
+
+ };
+
+
+ /**
+ * Check if node node bounds position is inside any ucpe on graph, and return the ucpe
+ * @param {diagram} the diagram.
+ * @param {nodeActualBounds} the actual bound position of the node.
+ * @return the ucpe if found else return null
+ */
+ public isInUcpe = (cy: Cy.Instance, nodeBounds: Cy.BoundingBox): Cy.CollectionElements => {
+
+ let ucpeNodes = cy.nodes('[?isUcpe]').filterFn((ucpeNode) => {
+ return this.isFirstBoxContainsInSecondBox(nodeBounds, ucpeNode.boundingbox());
+ });
+ return ucpeNodes;
+ };
+
+ /**
+ *
+ * @param cy
+ * @param node
+ * @returns {Array}
+ */
+ public getLinkableNodes(cy:Cy.Instance, node:Cy.CollectionFirstNode):Array<Models.Graph.CompositionCiNodeBase>{
+ let compatibleNodes = [];
+ _.each(cy.nodes(), (tempNode)=>{
+ if(this.nodeLocationsCompatible(cy, node, tempNode)){
+ compatibleNodes.push(tempNode.data());
+ }
+ });
+ return compatibleNodes;
+ }
+
+ /**
+ * Checks whether node locations are compatible in reference to UCPEs.
+ * Returns true if both nodes are in UCPE or both nodes out, or one node is UCPEpart.
+ * @param node1
+ * @param node2
+ */
+ public nodeLocationsCompatible(cy:Cy.Instance, node1:Cy.CollectionFirstNode, node2:Cy.CollectionFirstNode){
+
+ let ucpe = cy.nodes('[?isUcpe]');
+ if(!ucpe.length){ return true; }
+ if(node1.data().isUcpePart || node2.data().isUcpePart) { return true; }
+
+ return (this.isFirstBoxContainsInSecondBox(node1.boundingbox(), ucpe.boundingbox()) == this.isFirstBoxContainsInSecondBox(node2.boundingbox(), ucpe.boundingbox()));
+
+ }
+
+ /**
+ * This function will init qtip tooltip on the node
+ * @param node - the node we want the tooltip to apply on
+ */
+ public initNodeTooltip(node:Cy.CollectionNodes) {
+
+ let opts = {
+ content: function () {
+ return this.data('name');
+ },
+ position: {
+ my: 'top center',
+ at: 'bottom center',
+ adjust: {x:0, y:-5}
+ },
+ style: {
+ classes: 'qtip-dark qtip-rounded qtip-custom',
+ tip: {
+ width: 16,
+ height: 8
+ }
+ },
+ show: {
+ event: 'mouseover',
+ delay: 1000
+ },
+ hide: {event: 'mouseout mousedown'},
+ includeLabels: true
+ };
+
+ if (node.data().isUcpePart){ //fix tooltip positioning for UCPE-cps
+ opts.position.adjust = {x:0, y:20};
+ }
+
+ node.qtip(opts);
+ };
+ };
+
+
+
+ CommonGraphUtils.$inject = ['NodesFactory', 'LinksFactory'];
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/common/style/component-instances-nodes-style.ts b/catalog-ui/app/scripts/directives/graphs-v2/common/style/component-instances-nodes-style.ts
new file mode 100644
index 0000000000..2ec0174aa9
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/common/style/component-instances-nodes-style.ts
@@ -0,0 +1,259 @@
+/**
+ * Created by obarda on 12/18/2016.
+ */
+/**
+ * Created by obarda on 12/13/2016.
+ */
+/// <reference path="../../../../references"/>
+module Sdc.Graph.Utils.ComponentIntanceNodesStyle {
+
+ export function getCompositionGraphStyle():Array<Cy.Stylesheet> {
+ return [
+ {
+ selector: 'core',
+ css: {
+ 'shape': 'rectangle',
+ 'active-bg-size': 0,
+ 'selection-box-color': 'rgb(0, 159, 219)',
+ 'selection-box-opacity': 0.2,
+ 'selection-box-border-color': '#009fdb',
+ 'selection-box-border-width': 1
+
+ }
+ },
+ {
+ selector: 'node',
+ css: {
+ 'font-family': 'omnes-regular,sans-serif',
+ 'font-size': 14,
+ 'events': 'yes',
+ 'text-events': 'yes',
+ 'text-border-width': 15,
+ 'text-border-color': Sdc.Utils.Constants.GraphColors.NODE_UCPE,
+ 'text-margin-y': 5
+ }
+ },
+ {
+ selector: '.vf-node',
+ css: {
+ 'background-color': 'transparent',
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'background-image': 'data(img)',
+ 'width': 65,
+ 'height': 65,
+ 'background-opacity': 0,
+ "background-width": 65,
+ "background-height": 65,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-fit': 'cover',
+ 'background-clip': 'node',
+ 'overlay-color': Sdc.Utils.Constants.GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+
+ {
+ selector: '.service-node',
+ css: {
+ 'background-color': 'transparent',
+ 'label': 'data(displayName)',
+ 'events': 'yes',
+ 'text-events': 'yes',
+ 'background-image': 'data(img)',
+ 'width': 64,
+ 'height': 64,
+ "border-width": 0,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-opacity': 0,
+ 'overlay-color': Sdc.Utils.Constants.GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+ {
+ selector: '.cp-node',
+ css: {
+ 'background-color': 'rgb(255,255,255)',
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'background-image': 'data(img)',
+ 'background-width': 21,
+ 'background-height': 21,
+ 'width': 21,
+ 'height': 21,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-opacity': 0,
+ 'overlay-color': Sdc.Utils.Constants.GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+ {
+ selector: '.vl-node',
+ css: {
+ 'background-color': 'rgb(255,255,255)',
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'background-image': 'data(img)',
+ 'background-width': 21,
+ 'background-height': 21,
+ 'width': 21,
+ 'height': 21,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-opacity': 0,
+ 'overlay-color': Sdc.Utils.Constants.GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+ {
+ selector: '.ucpe-cp',
+ css: {
+ 'background-color': Sdc.Utils.Constants.GraphColors.NODE_UCPE_CP,
+ 'background-width': 15,
+ 'background-height': 15,
+ 'width': 15,
+ 'height': 15,
+ 'text-halign': 'center',
+ 'overlay-opacity': 0,
+ 'label': 'data(displayName)',
+ 'text-valign': 'data(textPosition)',
+ 'text-margin-y': (ele:Cy.Collection) => {
+ return (ele.data('textPosition') == 'top')? -5 : 5;
+ },
+ 'font-size': 12
+ }
+ },
+ {
+ selector: '.ucpe-node',
+ css: {
+ 'background-fit': 'cover',
+ 'padding-bottom': 0,
+ 'padding-top': 0
+ }
+ },
+ {
+ selector: '.simple-link',
+ css: {
+ 'width': 1,
+ 'line-color': Sdc.Utils.Constants.GraphColors.BASE_LINK,
+ 'target-arrow-color': '#3b7b9b',
+ 'target-arrow-shape': 'triangle',
+ 'curve-style': 'bezier',
+ 'control-point-step-size': 30
+ }
+ },
+ {
+ selector: '.vl-link',
+ css: {
+ 'width': 3,
+ 'line-color': Sdc.Utils.Constants.GraphColors.VL_LINK,
+ 'curve-style': 'bezier',
+ 'control-point-step-size': 30
+ }
+ },
+ {
+ selector: '.ucpe-host-link',
+ css: {
+ 'width': 0
+ }
+ },
+ {
+ selector: '.not-certified-link',
+ css: {
+ 'width': 1,
+ 'line-color': Sdc.Utils.Constants.GraphColors.NOT_CERTIFIED_LINK,
+ 'curve-style': 'bezier',
+ 'control-point-step-size': 30,
+ 'line-style': 'dashed',
+ 'target-arrow-color': '#3b7b9b',
+ 'target-arrow-shape': 'triangle'
+
+ }
+ },
+
+ {
+ selector: '.not-certified',
+ css: {
+ 'shape': 'rectangle',
+ 'background-image': (ele:Cy.Collection) => {
+ return ele.data().initImage(ele)
+ },
+ "border-width": 0
+ }
+ },
+ {
+ selector: 'node:selected',
+ css: {
+ "border-width": 2,
+ "border-color": Sdc.Utils.Constants.GraphColors.NODE_SELECTED_BORDER_COLOR,
+ 'shape': 'rectangle'
+ }
+ },
+ {
+ selector: 'edge:selected',
+ css: {
+ 'line-color': Sdc.Utils.Constants.GraphColors.ACTIVE_LINK
+
+ }
+ },
+ {
+ selector: 'edge:active',
+ css: {
+ 'overlay-opacity': 0
+ }
+ }
+ ]
+ }
+
+ export function getBasicNodeHanlde() {
+ return {
+ positionX: "center",
+ positionY: "top",
+ offsetX: 15,
+ offsetY: -20,
+ color: "#27a337",
+ type: "default",
+ single: false,
+ nodeTypeNames: ["basic-node"],
+ imageUrl: Sdc.Utils.Constants.IMAGE_PATH + '/styles/images/resource-icons/' + 'canvasPlusIcon.png',
+ lineWidth: 2,
+ lineStyle: 'dashed'
+
+ }
+ }
+
+ export function getBasicSmallNodeHandle() {
+ return {
+ positionX: "center",
+ positionY: "top",
+ offsetX: 3,
+ offsetY: -25,
+ color: "#27a337",
+ type: "default",
+ single: false,
+ nodeTypeNames: ["basic-small-node"],
+ imageUrl: Sdc.Utils.Constants.IMAGE_PATH + '/styles/images/resource-icons/' + 'canvasPlusIcon.png',
+ lineWidth: 2,
+ lineStyle: 'dashed'
+ }
+ }
+
+ export function getUcpeCpNodeHandle() {
+ return {
+ positionX: "center",
+ positionY: "center",
+ offsetX: -8,
+ offsetY: -10,
+ color: "#27a337",
+ type: "default",
+ single: false,
+ nodeTypeNames: ["ucpe-cp-node"],
+ imageUrl: Sdc.Utils.Constants.IMAGE_PATH + '/styles/images/resource-icons/' + 'canvasPlusIcon.png',
+ lineWidth: 2,
+ lineStyle: 'dashed'
+ }
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/common/style/module-node-style.ts b/catalog-ui/app/scripts/directives/graphs-v2/common/style/module-node-style.ts
new file mode 100644
index 0000000000..62436fbf74
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/common/style/module-node-style.ts
@@ -0,0 +1,92 @@
+/**
+ * Created by obarda on 1/1/2017.
+ */
+/**
+ * Created by obarda on 12/18/2016.
+ */
+/**
+ * Created by obarda on 12/13/2016.
+ */
+/// <reference path="../../../../references"/>
+module Sdc.Graph.Utils.ModulesNodesStyle {
+
+ export function getModuleGraphStyle():Array<Cy.Stylesheet> {
+
+ return [
+ {
+ selector: '.cy-expand-collapse-collapsed-node',
+ css: {
+ 'background-image': 'data(img)',
+ 'width': 34,
+ 'height': 32,
+ 'background-opacity': 0,
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'events': 'yes',
+ 'text-events': 'yes',
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'text-margin-y': 5,
+ 'border-opacity': 0
+ }
+ },
+ {
+ selector: '.module-node',
+ css: {
+ 'background-color': 'transparent',
+ 'background-opacity': 0,
+ "border-width": 2,
+ "border-color": Sdc.Utils.Constants.GraphColors.NODE_SELECTED_BORDER_COLOR,
+ 'border-style': 'dashed',
+ 'label': 'data(displayName)',
+ 'events': 'yes',
+ 'text-events': 'yes',
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'text-margin-y': 8
+ }
+ },
+ {
+ selector: 'node:selected',
+ css: {
+ "border-opacity": 0
+ }
+ },
+ {
+ selector: '.simple-link:selected',
+ css: {
+ 'line-color': Sdc.Utils.Constants.GraphColors.BASE_LINK,
+ }
+ },
+ {
+ selector: '.vl-link:selected',
+ css: {
+ 'line-color': Sdc.Utils.Constants.GraphColors.VL_LINK,
+ }
+ },
+ {
+ selector: '.cy-expand-collapse-collapsed-node:selected',
+ css: {
+ "border-color": Sdc.Utils.Constants.GraphColors.NODE_SELECTED_BORDER_COLOR,
+ 'border-opacity': 1,
+ 'border-style': 'solid',
+ 'border-width': 2
+ }
+ },
+ {
+ selector: '.module-node:selected',
+ css: {
+ "border-color": Sdc.Utils.Constants.GraphColors.NODE_SELECTED_BORDER_COLOR,
+ 'border-opacity': 1
+ }
+ },
+ {
+ selector: '.dummy-node',
+ css: {
+ 'width': 20,
+ 'height': 20
+ }
+ },
+ ]
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.directive.ts b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.directive.ts
new file mode 100644
index 0000000000..708f1d091a
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.directive.ts
@@ -0,0 +1,555 @@
+/// <reference path="../../../references"/>
+module Sdc.Directives {
+
+ import ComponentFactory = Sdc.Utils.ComponentFactory;
+ import LoaderService = Sdc.Services.LoaderService;
+ import GRAPH_EVENTS = Sdc.Utils.Constants.GRAPH_EVENTS;
+
+ interface ICompositionGraphScope extends ng.IScope {
+
+ component:Models.Components.Component;
+ isViewOnly:boolean;
+ // Link menu - create link menu
+ relationMenuDirectiveObj:Models.RelationMenuDirectiveObj;
+ isLinkMenuOpen:boolean;
+ createLinkFromMenu:(chosenMatch:Models.MatchBase, vl:Models.Components.Component)=>void;
+
+ //modify link menu - for now only delete menu
+ relationMenuTimeout:ng.IPromise<any>;
+ linkMenuObject:Models.LinkMenu;
+
+ //left palette functions callbacks
+ dropCallback(event:JQueryEventObject, ui:any):void;
+ beforeDropCallback(event:IDragDropEvent):void;
+ verifyDrop(event:JQueryEventObject, ui:any):void;
+
+ //Links menus
+ deleteRelation(link:Cy.CollectionEdges):void;
+ hideRelationMenu();
+ }
+
+ export class CompositionGraph implements ng.IDirective {
+ private _cy:Cy.Instance;
+ private _currentlyCLickedNodePosition:Cy.Position;
+ private $document:JQuery = $(document);
+ private dragElement:JQuery;
+ private dragComponent: Sdc.Models.ComponentsInstances.ComponentInstance;
+
+ constructor(private $q:ng.IQService,
+ private $filter:ng.IFilterService,
+ private $log:ng.ILogService,
+ private $timeout:ng.ITimeoutService,
+ private NodesFactory:Sdc.Utils.NodesFactory,
+ private CompositionGraphLinkUtils:Sdc.Graph.Utils.CompositionGraphLinkUtils,
+ private GeneralGraphUtils:Graph.Utils.CompositionGraphGeneralUtils,
+ private ComponentInstanceFactory:Utils.ComponentInstanceFactory,
+ private NodesGraphUtils:Sdc.Graph.Utils.CompositionGraphNodesUtils,
+ private eventListenerService:Services.EventListenerService,
+ private ComponentFactory:ComponentFactory,
+ private LoaderService:LoaderService,
+ private commonGraphUtils:Graph.Utils.CommonGraphUtils,
+ private matchCapabilitiesRequirementsUtils:Graph.Utils.MatchCapabilitiesRequirementsUtils) {
+
+ }
+
+ restrict = 'E';
+ templateUrl = '/app/scripts/directives/graphs-v2/composition-graph/composition-graph.html';
+ scope = {
+ component: '=',
+ isViewOnly: '='
+ };
+
+ link = (scope:ICompositionGraphScope, el:JQuery) => {
+ this.loadGraph(scope, el);
+
+ scope.$on('$destroy', () => {
+ this._cy.destroy();
+ _.forEach(GRAPH_EVENTS, (event) => {
+ this.eventListenerService.unRegisterObserver(event);
+ });
+ });
+
+ };
+
+ private loadGraph = (scope:ICompositionGraphScope, el:JQuery) => {
+
+
+ let graphEl = el.find('.sdc-composition-graph-wrapper');
+ this.initGraph(graphEl, scope.isViewOnly);
+ this.initGraphNodes(scope.component.componentInstances, scope.isViewOnly);
+ this.commonGraphUtils.initGraphLinks(this._cy, scope.component.componentInstancesRelations);
+ this.commonGraphUtils.initUcpeChildren(this._cy);
+ this.initDropZone(scope);
+ this.registerCytoscapeGraphEvents(scope);
+ this.registerCustomEvents(scope, el);
+ this.initViewMode(scope.isViewOnly);
+
+ };
+
+ private initGraph(graphEl:JQuery, isViewOnly:boolean) {
+
+ this._cy = cytoscape({
+ container: graphEl,
+ style: Sdc.Graph.Utils.ComponentIntanceNodesStyle.getCompositionGraphStyle(),
+ zoomingEnabled: false,
+ selectionType: 'single',
+ boxSelectionEnabled: true,
+ autolock: isViewOnly,
+ autoungrabify: isViewOnly
+ });
+ }
+
+ private initViewMode(isViewOnly:boolean) {
+
+ if (isViewOnly) {
+ //remove event listeners
+ this._cy.off('drag');
+ this._cy.off('handlemouseout');
+ this._cy.off('handlemouseover');
+ this._cy.edges().unselectify();
+ }
+ };
+
+ private registerCustomEvents(scope:ICompositionGraphScope, el:JQuery) {
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, (component:Models.DisplayComponent) => {
+ this.$log.info(`composition-graph::registerEventServiceEvents:: palette hover on component: ${component.uniqueId}`);
+
+ let nodesData = this.NodesGraphUtils.getAllNodesData(this._cy.nodes());
+ let nodesLinks = this.GeneralGraphUtils.getAllCompositionCiLinks(this._cy);
+
+ if (this.GeneralGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(component.uniqueId)) {
+ let cacheComponent = this.GeneralGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(component.uniqueId);
+ let filteredNodesData = this.matchCapabilitiesRequirementsUtils.findByMatchingCapabilitiesToRequirements(cacheComponent, nodesData, nodesLinks);
+
+ this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, this._cy);
+ this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, this._cy);
+
+ return;
+ }
+
+ component.component.updateRequirementsCapabilities()
+ .then((res) => {
+ component.component.capabilities = res.capabilities;
+ component.component.requirements = res.requirements;
+
+ let filteredNodesData = this.matchCapabilitiesRequirementsUtils.findByMatchingCapabilitiesToRequirements(component.component, nodesData, nodesLinks);
+ this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, this._cy);
+ this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, this._cy)
+ });
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT, () => {
+ this._cy.emit('hidehandles');
+ this.matchCapabilitiesRequirementsUtils.resetFadedNodes(this._cy);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_START, (dragElement, dragComponent) => {
+
+ this.dragElement = dragElement;
+ this.dragComponent = this.ComponentInstanceFactory.createComponentInstanceFromComponent(dragComponent);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_ACTION, (event:IDragDropEvent) => {
+ this._onComponentDrag(event);
+
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_NAME_CHANGED, (component:Models.ComponentsInstances.ComponentInstance) => {
+
+ let selectedNode = this._cy.getElementById(component.uniqueId);
+ selectedNode.data().componentInstance.name = component.name;
+ selectedNode.data('displayName', selectedNode.data().getDisplayName());
+
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, (componentInstance:Models.ComponentsInstances.ComponentInstance) => {
+ let nodeToDelete = this._cy.getElementById(componentInstance.uniqueId);
+ this.NodesGraphUtils.deleteNode(this._cy, scope.component, nodeToDelete);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_MULTIPLE_COMPONENTS, () => {
+
+ this._cy.$('node:selected').each((i:number, node:Cy.CollectionNodes) => {
+ this.NodesGraphUtils.deleteNode(this._cy, scope.component, node);
+ });
+
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_EDGE, (releaseLoading:boolean, linksToDelete:Cy.CollectionEdges) => {
+ this.CompositionGraphLinkUtils.deleteLink(this._cy, scope.component, releaseLoading, linksToDelete);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_INSERT_NODE_TO_UCPE, (node:Cy.CollectionNodes, ucpe:Cy.CollectionNodes, updateExistingNode: boolean) => {
+
+ this.commonGraphUtils.initUcpeChildData(node, ucpe);
+ //check if item is a VL, and if so, skip adding the binding to ucpe
+ if(!(node.data() instanceof Sdc.Models.Graph.CompositionCiNodeVl)){
+ this.CompositionGraphLinkUtils.createVfToUcpeLink(scope.component, this._cy, ucpe.data(), node.data()); //create link from the node to the ucpe
+ }
+
+ if(updateExistingNode){
+ let vlsPendingDeletion:Cy.CollectionNodes = this.NodesGraphUtils.deleteNodeVLsUponMoveToOrFromUCPE(scope.component, node.cy(), node); //delete connected VLs that no longer have 2 links
+ this.CompositionGraphLinkUtils.deleteLinksWhenNodeMovedFromOrToUCPE(scope.component, node.cy(), node, vlsPendingDeletion); //delete all connected links if needed
+ this.GeneralGraphUtils.pushUpdateComponentInstanceActionToQueue(scope.component, true, node.data().componentInstance); //update componentInstance position
+ }
+
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_REMOVE_NODE_FROM_UCPE, (node:Cy.CollectionNodes, ucpe:Cy.CollectionNodes) => {
+ this.commonGraphUtils.removeUcpeChildData(node);
+ let vlsPendingDeletion:Cy.CollectionNodes = this.NodesGraphUtils.deleteNodeVLsUponMoveToOrFromUCPE(scope.component, node.cy(), node);
+ this.CompositionGraphLinkUtils.deleteLinksWhenNodeMovedFromOrToUCPE(scope.component, node.cy(), node, vlsPendingDeletion); //delete all connected links if needed
+ this.GeneralGraphUtils.pushUpdateComponentInstanceActionToQueue(scope.component, true, node.data().componentInstance); //update componentInstance position
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_VERSION_CHANGED, (component:Models.Components.Component) => {
+ scope.component = component;
+ this.loadGraph(scope, el);
+ });
+
+
+ scope.createLinkFromMenu = (chosenMatch:Models.MatchBase, vl:Models.Components.Component):void => {
+ scope.isLinkMenuOpen = false;
+
+ this.CompositionGraphLinkUtils.createLinkFromMenu(this._cy, chosenMatch, vl, scope.component);
+ };
+
+ scope.hideRelationMenu = () => {
+ this.commonGraphUtils.safeApply(scope, () => {
+ scope.linkMenuObject = null;
+ this.$timeout.cancel(scope.relationMenuTimeout);
+ });
+ };
+
+
+ scope.deleteRelation = (link:Cy.CollectionEdges) => {
+ scope.hideRelationMenu();
+
+ //if multiple edges selected, delete the VL itself so edges get deleted automatically
+ if (this._cy.$('edge:selected').length > 1) {
+ this.NodesGraphUtils.deleteNode(this._cy, scope.component, this._cy.$('node:selected'));
+ } else {
+ this.CompositionGraphLinkUtils.deleteLink(this._cy, scope.component, true, link);
+ }
+ };
+ }
+
+
+ private registerCytoscapeGraphEvents(scope:ICompositionGraphScope) {
+
+ this._cy.on('addedgemouseup', (event, data) => {
+ scope.relationMenuDirectiveObj = this.CompositionGraphLinkUtils.onLinkDrawn(this._cy, data.source, data.target);
+ if (scope.relationMenuDirectiveObj != null) {
+ scope.$apply(() => {
+ scope.isLinkMenuOpen = true;
+ });
+ }
+ });
+ this._cy.on('tapstart', 'node', (event:Cy.EventObject) => {
+ this._currentlyCLickedNodePosition = angular.copy(event.cyTarget[0].position()); //update node position on drag
+ if(event.cyTarget.data().isUcpe){
+ this._cy.nodes('.ucpe-cp').unlock();
+ event.cyTarget.style('opacity', 0.5);
+ }
+ });
+
+ this._cy.on('drag', 'node', (event:Cy.EventObject) => {
+
+ if (event.cyTarget.data().isDraggable) {
+ event.cyTarget.style({'overlay-opacity': 0.24});
+ if (this.GeneralGraphUtils.isValidDrop(this._cy, event.cyTarget)) {
+ event.cyTarget.style({'overlay-color': Utils.Constants.GraphColors.NODE_BACKGROUND_COLOR});
+ } else {
+ event.cyTarget.style({'overlay-color': Utils.Constants.GraphColors.NODE_OVERLAPPING_BACKGROUND_COLOR});
+ }
+ }
+
+ if(event.cyTarget.data().isUcpe){
+ let pos = event.cyTarget.position();
+
+ this._cy.nodes('[?isInsideGroup]').positions((i, node)=>{
+ return {
+ x: pos.x + node.data("ucpeOffset").x,
+ y: pos.y + node.data("ucpeOffset").y
+ }
+ });
+ }
+ });
+
+
+ this._cy.on('handlemouseover', (event, payload) => {
+
+ if (payload.node.grabbed()) { //no need to add opacity while we are dragging and hovering othe nodes
+ return;
+ }
+
+ let nodesData = this.NodesGraphUtils.getAllNodesData(this._cy.nodes());
+ let nodesLinks = this.GeneralGraphUtils.getAllCompositionCiLinks(this._cy);
+
+ let linkableNodes = this.commonGraphUtils.getLinkableNodes(this._cy, payload.node);
+ let filteredNodesData = this.matchCapabilitiesRequirementsUtils.findByMatchingCapabilitiesToRequirements(payload.node.data().componentInstance, linkableNodes, nodesLinks);
+ this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, this._cy);
+ this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, this._cy, payload.node.data());
+
+ });
+
+ this._cy.on('handlemouseout', () => {
+ this._cy.emit('hidehandles');
+ this.matchCapabilitiesRequirementsUtils.resetFadedNodes(this._cy);
+ });
+
+
+ this._cy.on('tapend', (event:Cy.EventObject) => {
+
+ if (event.cyTarget === this._cy) { //On Background clicked
+ if (this._cy.$('node:selected').length === 0) { //if the background click but not dragged
+ this.eventListenerService.notifyObservers(Sdc.Utils.Constants.GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED);
+ }
+ scope.hideRelationMenu();
+ }
+
+ else if (event.cyTarget.isEdge()) { //On Edge clicked
+ if (scope.isViewOnly) return;
+ this.CompositionGraphLinkUtils.handleLinkClick(this._cy, event);
+ this.openModifyLinkMenu(scope, this.CompositionGraphLinkUtils.getModifyLinkMenu(event.cyTarget[0], event), 6000);
+ }
+
+ else { //On Node clicked
+ this._cy.nodes(':grabbed').style({'overlay-opacity': 0});
+
+ let isUcpe:boolean = event.cyTarget.data().isUcpe;
+ let newPosition = event.cyTarget[0].position();
+ //node position changed (drop after drag event) - we need to update position
+ if (this._currentlyCLickedNodePosition.x !== newPosition.x || this._currentlyCLickedNodePosition.y !== newPosition.y) {
+ let nodesMoved:Cy.CollectionNodes = this._cy.$(':grabbed');
+ if(isUcpe){
+ nodesMoved = nodesMoved.add(this._cy.nodes('[?isInsideGroup]:free')); //'child' nodes will not be recognized as "grabbed" elements within cytoscape. manually add them to collection of nodes moved.
+ }
+ this.NodesGraphUtils.onNodesPositionChanged(this._cy, scope.component, nodesMoved);
+ } else {
+ this.$log.debug('composition-graph::onNodeSelectedEvent:: fired');
+ scope.$apply(() => {
+ this.eventListenerService.notifyObservers(Sdc.Utils.Constants.GRAPH_EVENTS.ON_NODE_SELECTED, event.cyTarget.data().componentInstance);
+ });
+ }
+
+ if(isUcpe){
+ this._cy.nodes('.ucpe-cp').lock();
+ event.cyTarget.style('opacity', 1);
+ }
+
+ }
+ });
+
+ this._cy.on('boxselect', 'node', (event:Cy.EventObject) => {
+ this.eventListenerService.notifyObservers(Utils.Constants.GRAPH_EVENTS.ON_NODE_SELECTED, event.cyTarget.data().componentInstance);
+ });
+ }
+
+ private openModifyLinkMenu = (scope:ICompositionGraphScope, linkMenuObject:Models.LinkMenu, timeOutInMilliseconds?:number) => {
+
+ this.commonGraphUtils.safeApply(scope, () => {
+ scope.linkMenuObject = linkMenuObject;
+ });
+
+ scope.relationMenuTimeout = this.$timeout(() => {
+ scope.hideRelationMenu();
+ }, timeOutInMilliseconds ? timeOutInMilliseconds : 6000);
+ };
+
+ private initGraphNodes(componentInstances:Models.ComponentsInstances.ComponentInstance[], isViewOnly:boolean) {
+
+ if (!isViewOnly) { //Init nodes handle extension - enable dynamic links
+ setTimeout(()=> {
+ let handles = new CytoscapeEdgeEditation;
+ handles.init(this._cy, 18);
+ handles.registerHandle(Sdc.Graph.Utils.ComponentIntanceNodesStyle.getBasicNodeHanlde());
+ handles.registerHandle(Sdc.Graph.Utils.ComponentIntanceNodesStyle.getBasicSmallNodeHandle());
+ handles.registerHandle(Sdc.Graph.Utils.ComponentIntanceNodesStyle.getUcpeCpNodeHandle());
+ }, 0);
+ }
+
+ _.each(componentInstances, (instance) => {
+ let compositionGraphNode:Models.Graph.CompositionCiNodeBase = this.NodesFactory.createNode(instance);
+ this.commonGraphUtils.addComponentInstanceNodeToGraph(this._cy, compositionGraphNode);
+ });
+
+
+
+ }
+
+
+ private initDropZone(scope:ICompositionGraphScope) {
+
+ if (scope.isViewOnly) {
+ return;
+ }
+ scope.dropCallback = (event:IDragDropEvent) => {
+ this.$log.debug(`composition-graph::dropCallback:: fired`);
+ this.addNode(event, scope);
+ };
+
+ scope.verifyDrop = (event:JQueryEventObject) => {
+
+ if(this.dragElement.hasClass('red')){
+ return false;
+ }
+ return true;
+ };
+
+ scope.beforeDropCallback = (event:IDragDropEvent): ng.IPromise<void> => {
+ let deferred: ng.IDeferred<void> = this.$q.defer<void>();
+ if(this.dragElement.hasClass('red')){
+ deferred.reject();
+ } else {
+ deferred.resolve();
+ }
+
+ return deferred.promise;
+ }
+ }
+
+ private _getNodeBBox(event:IDragDropEvent, position?:Cy.Position) {
+ let bbox = <Cy.BoundingBox>{};
+ if (!position) {
+ position = this.commonGraphUtils.getCytoscapeNodePosition(this._cy, event);
+ }
+ let cushionWidth:number = 40;
+ let cushionHeight:number = 40;
+
+ bbox.x1 = position.x - cushionWidth / 2;
+ bbox.y1 = position.y - cushionHeight / 2;
+ bbox.x2 = position.x + cushionWidth / 2;
+ bbox.y2 = position.y + cushionHeight / 2;
+ return bbox;
+ }
+
+ private createComponentInstanceOnGraphFromComponent(fullComponent:Models.Components.Component, event:IDragDropEvent, scope:ICompositionGraphScope) {
+
+ let componentInstanceToCreate:Models.ComponentsInstances.ComponentInstance = this.ComponentInstanceFactory.createComponentInstanceFromComponent(fullComponent);
+ let cytoscapePosition:Cy.Position = this.commonGraphUtils.getCytoscapeNodePosition(this._cy, event);
+
+ componentInstanceToCreate.posX = cytoscapePosition.x;
+ componentInstanceToCreate.posY = cytoscapePosition.y;
+
+
+ let onFailedCreatingInstance:(error:any) => void = (error:any) => {
+ this.LoaderService.hideLoader('composition-graph');
+ };
+
+ //on success - update node data
+ let onSuccessCreatingInstance = (createInstance:Models.ComponentsInstances.ComponentInstance):void => {
+
+ this.LoaderService.hideLoader('composition-graph');
+
+ createInstance.name = this.$filter('resourceName')(createInstance.name);
+ createInstance.requirements = new Models.RequirementsGroup(fullComponent.requirements);
+ createInstance.capabilities = new Models.CapabilitiesGroup(fullComponent.capabilities);
+ createInstance.componentVersion = fullComponent.version;
+ createInstance.icon = fullComponent.icon;
+ createInstance.setInstanceRC();
+
+ let newNode:Models.Graph.CompositionCiNodeBase = this.NodesFactory.createNode(createInstance);
+ let cyNode:Cy.CollectionNodes = this.commonGraphUtils.addComponentInstanceNodeToGraph(this._cy, newNode);
+
+ //check if node was dropped into a UCPE
+ let ucpe:Cy.CollectionElements = this.commonGraphUtils.isInUcpe(this._cy, cyNode.boundingbox());
+ if (ucpe.length > 0) {
+ this.eventListenerService.notifyObservers(Utils.Constants.GRAPH_EVENTS.ON_INSERT_NODE_TO_UCPE, cyNode, ucpe, false);
+ }
+
+ };
+
+ // Create the component instance on server
+ this.GeneralGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIAction(() => {
+ scope.component.createComponentInstance(componentInstanceToCreate).then(onSuccessCreatingInstance, onFailedCreatingInstance);
+ });
+ }
+
+ private _onComponentDrag(event:IDragDropEvent) {
+
+ if(event.clientX < Sdc.Utils.Constants.GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET || event.clientY < Sdc.Utils.Constants.GraphUIObjects.DIAGRAM_HEADER_OFFSET){ //hovering over palette. Dont bother computing validity of drop
+ this.dragElement.removeClass('red');
+ return;
+ }
+
+ let offsetPosition = {x: event.clientX - Sdc.Utils.Constants.GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET, y: event.clientY - Sdc.Utils.Constants.GraphUIObjects.DIAGRAM_HEADER_OFFSET}
+ let bbox = this._getNodeBBox(event, offsetPosition);
+
+ if (this.GeneralGraphUtils.isPaletteDropValid(this._cy, bbox, this.dragComponent)) {
+ this.dragElement.removeClass('red');
+ } else {
+ this.dragElement.addClass('red');
+ }
+ }
+
+ private addNode(event:IDragDropEvent, scope:ICompositionGraphScope) {
+ this.LoaderService.showLoader('composition-graph');
+
+ this.$log.debug('composition-graph::addNode:: fired');
+ let draggedComponent:Models.Components.Component = event.dataTransfer.component;
+
+ if (this.GeneralGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(draggedComponent.uniqueId)) {
+ this.$log.debug('composition-graph::addNode:: capabilities found in cache, creating component');
+ let fullComponent = this.GeneralGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(draggedComponent.uniqueId);
+ this.createComponentInstanceOnGraphFromComponent(fullComponent, event, scope);
+ return;
+ }
+
+ this.$log.debug('composition-graph::addNode:: capabilities not found, requesting from server');
+ this.ComponentFactory.getComponentFromServer(draggedComponent.getComponentSubType(), draggedComponent.uniqueId)
+ .then((fullComponent:Models.Components.Component) => {
+ this.createComponentInstanceOnGraphFromComponent(fullComponent, event, scope);
+ });
+ }
+
+ public static factory = ($q,
+ $filter,
+ $log,
+ $timeout,
+ NodesFactory,
+ LinksGraphUtils,
+ GeneralGraphUtils,
+ ComponentInstanceFactory,
+ NodesGraphUtils,
+ EventListenerService,
+ ComponentFactory,
+ LoaderService,
+ CommonGraphUtils,
+ MatchCapabilitiesRequirementsUtils) => {
+ return new CompositionGraph(
+ $q,
+ $filter,
+ $log,
+ $timeout,
+ NodesFactory,
+ LinksGraphUtils,
+ GeneralGraphUtils,
+ ComponentInstanceFactory,
+ NodesGraphUtils,
+ EventListenerService,
+ ComponentFactory,
+ LoaderService,
+ CommonGraphUtils,
+ MatchCapabilitiesRequirementsUtils);
+ }
+ }
+
+ CompositionGraph.factory.$inject = [
+ '$q',
+ '$filter',
+ '$log',
+ '$timeout',
+ 'NodesFactory',
+ 'CompositionGraphLinkUtils',
+ 'CompositionGraphGeneralUtils',
+ 'ComponentInstanceFactory',
+ 'CompositionGraphNodesUtils',
+ 'EventListenerService',
+ 'ComponentFactory',
+ 'LoaderService',
+ 'CommonGraphUtils',
+ 'MatchCapabilitiesRequirementsUtils'
+ ];
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.html b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.html
new file mode 100644
index 0000000000..5f2c488341
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.html
@@ -0,0 +1,22 @@
+<loader display="isLoading" loader-type="composition-graph"></loader>
+<div class="sdc-composition-graph-wrapper" ng-class="{'view-only':isViewOnly}"
+ data-drop="true"
+ data-jqyoui-options="{accept: verifyDrop}"
+ data-jqyoui-droppable="{onDrop:'dropCallback', beforeDrop: 'beforeDropCallback'}">
+</div>
+
+<relation-menu relation-menu-directive-obj="relationMenuDirectiveObj" is-link-menu-open="isLinkMenuOpen"
+ create-relation="createLinkFromMenu" cancel="cancelRelationMenu()"></relation-menu>
+
+
+<div class="w-sdc-canvas-menu"
+ data-ng-show="linkMenuObject" ng-style="{left: linkMenuObject.position.x, top: linkMenuObject.position.y}"
+ id="relationMenu">
+
+ <div class="w-sdc-canvas-menu-content hand" data-ng-click="deleteRelation(linkMenuObject.link)">
+ <div class="w-sdc-canvas-menu-content-delete-button"></div>
+ <!--{{relationComponent.data.relation.relationships[0].relationship.type | relationName }}-->
+ Delete
+ </div>
+
+</div>
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.less b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.less
new file mode 100644
index 0000000000..7b999967b7
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.less
@@ -0,0 +1,14 @@
+composition-graph {
+ display: block;
+
+ height:100%;
+ width: 100%;
+ .sdc-composition-graph-wrapper{
+ height:100%;
+ width: 100%;
+ }
+
+ &.view-only{
+ background-color:rgb(248, 248, 248);
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-general-utils.ts b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-general-utils.ts
new file mode 100644
index 0000000000..495a243d75
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-general-utils.ts
@@ -0,0 +1,243 @@
+/// <reference path="../../../../references"/>
+module Sdc.Graph.Utils {
+
+ import Dictionary = Sdc.Utils.Dictionary;
+
+ export class CompositionGraphGeneralUtils {
+
+ public componentRequirementsAndCapabilitiesCaching = new Dictionary<string, Models.Components.Component>();
+ protected static graphUtilsUpdateQueue: Sdc.Utils.Functions.QueueUtils;
+
+ constructor(private $q: ng.IQService,
+ private LoaderService: Services.LoaderService,
+ private commonGraphUtils: Sdc.Graph.Utils.CommonGraphUtils,
+ private matchCapabilitiesRequirementsUtils: Graph.Utils.MatchCapabilitiesRequirementsUtils) {
+ CompositionGraphGeneralUtils.graphUtilsUpdateQueue = new Sdc.Utils.Functions.QueueUtils(this.$q);
+ }
+
+
+ /**
+ * Get the offset for the link creation Menu
+ * @param point
+ * @returns {Cy.Position}
+ */
+ public calcMenuOffset: Function = (point: Cy.Position): Cy.Position => {
+ point.x = point.x + 60;
+ point.y = point.y + 105;
+ return point;
+ };
+
+ /**
+ * return the top left position of the link menu
+ * @param cy
+ * @param targetNodePosition
+ * @returns {Cy.Position}
+ */
+ public getLinkMenuPosition = (cy: Cy.Instance, targetNodePosition: Cy.Position) => {
+ let menuPosition: Cy.Position = this.calcMenuOffset(targetNodePosition); //get the link mid point
+ if (document.body.scrollHeight < menuPosition.y + Sdc.Utils.Constants.GraphUIObjects.LINK_MENU_HEIGHT + $(document.getElementsByClassName('sdc-composition-graph-wrapper')).offset().top) { // if position menu is overflow bottom
+ menuPosition.y = document.body.scrollHeight - Sdc.Utils.Constants.GraphUIObjects.TOP_HEADER_HEIGHT - Sdc.Utils.Constants.GraphUIObjects.LINK_MENU_HEIGHT;
+ }
+ return menuPosition;
+ };
+
+
+ /**
+ * will return true/false if two nodes overlapping
+ *
+ * @param graph node
+ */
+ private isNodesOverlapping(node: Cy.CollectionFirstNode, draggedNode: Cy.CollectionFirstNode): boolean {
+
+ let nodeBoundingBox: Cy.BoundingBox = node.renderedBoundingBox();
+ let secondNodeBoundingBox: Cy.BoundingBox = draggedNode.renderedBoundingBox();
+
+ return this.isBBoxOverlapping(nodeBoundingBox, secondNodeBoundingBox);
+ }
+
+ /**
+ * Checks whether the bounding boxes of two nodes are overlapping on any side
+ * @param nodeOneBBox
+ * @param nodeTwoBBox
+ * @returns {boolean}
+ */
+ private isBBoxOverlapping(nodeOneBBox: Cy.BoundingBox, nodeTwoBBox: Cy.BoundingBox) {
+ return (((nodeOneBBox.x1 < nodeTwoBBox.x1 && nodeOneBBox.x2 > nodeTwoBBox.x1) ||
+ (nodeOneBBox.x1 < nodeTwoBBox.x2 && nodeOneBBox.x2 > nodeTwoBBox.x2) ||
+ (nodeTwoBBox.x1 < nodeOneBBox.x1 && nodeTwoBBox.x2 > nodeOneBBox.x2)) &&
+ ((nodeOneBBox.y1 < nodeTwoBBox.y1 && nodeOneBBox.y2 > nodeTwoBBox.y1) ||
+ (nodeOneBBox.y1 < nodeTwoBBox.y2 && nodeOneBBox.y2 > nodeTwoBBox.y2) ||
+ (nodeTwoBBox.y1 < nodeOneBBox.y1 && nodeTwoBBox.y2 > nodeOneBBox.y2)))
+ }
+
+
+ /**
+ * Checks whether a specific component instance can be hosted on the UCPE instance
+ * @param cy - Cytoscape instance
+ * @param fromUcpeInstance
+ * @param toComponentInstance
+ * @returns {Models.MatchReqToCapability}
+ */
+ public canBeHostedOn(cy: Cy.Instance, fromUcpeInstance: Models.ComponentsInstances.ComponentInstance, toComponentInstance: Models.ComponentsInstances.ComponentInstance): Models.MatchReqToCapability {
+
+ let matches: Array<Models.MatchBase> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromUcpeInstance, toComponentInstance, this.getAllCompositionCiLinks(cy));
+ let hostedOnMatch: Models.MatchBase = _.find(matches, (match: Models.MatchReqToCapability) => {
+ return match.requirement.capability.toLowerCase() === 'tosca.capabilities.container';
+ });
+
+ return <Models.MatchReqToCapability>hostedOnMatch;
+ };
+
+
+ /**
+ * Checks whether node can be dropped into UCPE
+ * @param cy
+ * @param nodeToInsert
+ * @param ucpeNode
+ * @returns {boolean}
+ */
+ private isValidDropInsideUCPE(cy: Cy.Instance, nodeToInsert: Models.ComponentsInstances.ComponentInstance, ucpeNode: Models.ComponentsInstances.ComponentInstance): boolean {
+
+ let hostedOnMatch: Models.MatchReqToCapability = this.canBeHostedOn(cy, ucpeNode, nodeToInsert);
+ let result: boolean = !angular.isUndefined(hostedOnMatch) || nodeToInsert.isVl(); //group validation
+ return result;
+
+ };
+
+
+ /**
+ * For drops from palette, checks whether the node can be dropped. If node is being held over another node, check if capable of hosting
+ * @param cy
+ * @param pseudoNodeBBox
+ * @param paletteComponentInstance
+ * @returns {boolean}
+ */
+ public isPaletteDropValid(cy: Cy.Instance, pseudoNodeBBox: Cy.BoundingBox, paletteComponentInstance:Sdc.Models.ComponentsInstances.ComponentInstance) {
+
+ let componentIsUCPE:boolean = (paletteComponentInstance.capabilities && paletteComponentInstance.capabilities['tosca.capabilities.Container'] && paletteComponentInstance.name.toLowerCase().indexOf('ucpe') > -1);
+
+ if(componentIsUCPE && cy.nodes('[?isUcpe]').length > 0) { //second UCPE not allowed
+ return false;
+ }
+
+ let illegalOverlappingNodes = _.filter(cy.nodes("[isSdcElement]"), (graphNode: Cy.CollectionFirstNode) => {
+
+ if(this.isBBoxOverlapping(pseudoNodeBBox, graphNode.renderedBoundingBox())){
+ if (!componentIsUCPE && graphNode.data().isUcpe) {
+ return !this.isValidDropInsideUCPE(cy, paletteComponentInstance, graphNode.data().componentInstance); //if this is valid insert into ucpe, we return false - no illegal overlapping nodes
+ }
+ return true;
+ }
+
+ return false;
+ });
+
+ return illegalOverlappingNodes.length === 0;
+ }
+
+ /**
+ * will return true/false if a drop of a single node is valid
+ *
+ * @param graph node
+ */
+ public isValidDrop(cy: Cy.Instance, draggedNode: Cy.CollectionFirstNode): boolean {
+
+ let illegalOverlappingNodes = _.filter(cy.nodes("[isSdcElement]"), (graphNode: Cy.CollectionFirstNode) => { //all sdc nodes, removing child nodes (childe node allways collaps
+
+ if (draggedNode.data().isUcpe && (graphNode.isChild() || graphNode.data().isInsideGroup)) { //ucpe cps always inside ucpe, no overlapping
+ return false;
+ }
+ if(draggedNode.data().isInsideGroup && (!draggedNode.active() || graphNode.data().isUcpe)) {
+ return false;
+ }
+
+ if (!draggedNode.data().isUcpe && !(draggedNode.data() instanceof Sdc.Models.Graph.CompositionCiNodeUcpeCp) && graphNode.data().isUcpe) { //case we are dragging a node into UCPE
+ let isEntirelyInUCPE:boolean = this.commonGraphUtils.isFirstBoxContainsInSecondBox(draggedNode.renderedBoundingBox(), graphNode.renderedBoundingBox());
+ if (isEntirelyInUCPE){
+ if(this.isValidDropInsideUCPE(cy, draggedNode.data().componentInstance, graphNode.data().componentInstance)){ //if this is valid insert into ucpe, we return false - no illegal overlapping nodes
+ return false;
+ }
+ }
+ }
+ return graphNode.data().id !== draggedNode.data().id && this.isNodesOverlapping(draggedNode, graphNode);
+
+ });
+ // return false;
+ return illegalOverlappingNodes.length === 0;
+ };
+
+ /**
+ * will return true/false if the move of the nodes is valid (no node overlapping and verifying if insert into UCPE is valid)
+ *
+ * @param nodesArray - the selected drags nodes
+ */
+ public isGroupValidDrop(cy: Cy.Instance, nodesArray: Cy.CollectionNodes): boolean {
+ var filterDraggedNodes = nodesArray.filter('[?isDraggable]');
+ let isValidDrop = _.every(filterDraggedNodes, (node: Cy.CollectionFirstNode) => {
+ return this.isValidDrop(cy, node);
+
+ });
+ return isValidDrop;
+ };
+
+ /**
+ * get all links in diagram
+ * @param cy
+ * @returns {any[]|boolean[]}
+ */
+ public getAllCompositionCiLinks = (cy: Cy.Instance): Array<Models.CompositionCiLinkBase> => {
+ return _.map(cy.edges("[isSdcElement]"), (edge: Cy.CollectionEdges) => {
+ return edge.data();
+ });
+ };
+
+
+ /**
+ * Get Graph Utils server queue
+ * @returns {Sdc.Utils.Functions.QueueUtils}
+ */
+ public getGraphUtilsServerUpdateQueue(): Sdc.Utils.Functions.QueueUtils {
+ return CompositionGraphGeneralUtils.graphUtilsUpdateQueue;
+ }
+ ;
+
+ /**
+ *
+ * @param blockAction - true/false if this is a block action
+ * @param instances
+ * @param component
+ */
+ public pushMultipleUpdateComponentInstancesRequestToQueue = (blockAction: boolean, instances: Array<Models.ComponentsInstances.ComponentInstance>, component: Models.Components.Component): void => {
+ if (blockAction) {
+ this.getGraphUtilsServerUpdateQueue().addBlockingUIAction(
+ () => component.updateMultipleComponentInstances(instances)
+ );
+ } else {
+ this.getGraphUtilsServerUpdateQueue().addNonBlockingUIAction(
+ () => component.updateMultipleComponentInstances(instances),
+ () => this.LoaderService.hideLoader('composition-graph'));
+ }
+ };
+
+ /**
+ * this function will update component instance data
+ * @param blockAction - true/false if this is a block action
+ * @param updatedInstance
+ */
+ public pushUpdateComponentInstanceActionToQueue = (component: Models.Components.Component, blockAction: boolean, updatedInstance: Models.ComponentsInstances.ComponentInstance): void => {
+
+ if (blockAction) {
+ this.LoaderService.showLoader('composition-graph');
+ this.getGraphUtilsServerUpdateQueue().addBlockingUIAction(
+ () => component.updateComponentInstance(updatedInstance)
+ );
+ } else {
+ this.getGraphUtilsServerUpdateQueue().addNonBlockingUIAction(
+ () => component.updateComponentInstance(updatedInstance),
+ () => this.LoaderService.hideLoader('composition-graph'));
+ }
+ };
+ }
+
+ CompositionGraphGeneralUtils.$inject = ['$q', 'LoaderService', 'CommonGraphUtils', 'MatchCapabilitiesRequirementsUtils'];
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-links-utils.ts b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-links-utils.ts
new file mode 100644
index 0000000000..602e6b6def
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-links-utils.ts
@@ -0,0 +1,347 @@
+/**
+ * Created by obarda on 6/28/2016.
+ */
+/// <reference path="../../../../references"/>
+module Sdc.Graph.Utils {
+
+ import ImageCreatorService = Sdc.Utils.ImageCreatorService;
+ import Module = Sdc.Models.Module;
+ export class CompositionGraphLinkUtils {
+
+ private p2pVL:Models.Components.Component;
+ private mp2mpVL:Models.Components.Component;
+
+ constructor(private linksFactory:Sdc.Utils.LinksFactory,
+ private loaderService:Services.LoaderService,
+ private generalGraphUtils:Sdc.Graph.Utils.CompositionGraphGeneralUtils,
+ private leftPaletteLoaderService:Services.Components.LeftPaletteLoaderService,
+ private componentInstanceFactory:Sdc.Utils.ComponentInstanceFactory,
+ private nodesFactory:Sdc.Utils.NodesFactory,
+ private commonGraphUtils: Sdc.Graph.Utils.CommonGraphUtils,
+ private matchCapabilitiesRequirementsUtils: Graph.Utils.MatchCapabilitiesRequirementsUtils) {
+
+ this.initScopeVls();
+
+ }
+
+
+ /**
+ * Delete the link on server and then remove it from graph
+ * @param component
+ * @param releaseLoading - true/false release the loader when finished
+ * @param link - the link to delete
+ */
+ public deleteLink = (cy:Cy.Instance, component:Models.Components.Component, releaseLoading:boolean, link:Cy.CollectionEdges) => {
+
+ this.loaderService.showLoader('composition-graph');
+ let onSuccessDeleteRelation = (response) => {
+ cy.remove(link);
+ };
+
+ if (!releaseLoading) {
+ this.generalGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIAction(
+ () => component.deleteRelation(link.data().relation).then(onSuccessDeleteRelation)
+ );
+ } else {
+ this.generalGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIActionWithReleaseCallback(
+ () => component.deleteRelation(link.data().relation).then(onSuccessDeleteRelation),
+ () => this.loaderService.hideLoader('composition-graph'));
+ }
+ };
+
+ /**
+ * create the link on server and than draw it on graph
+ * @param link - the link to create
+ * @param cy
+ * @param component
+ */
+ public createLink = (link:Models.CompositionCiLinkBase, cy:Cy.Instance, component:Models.Components.Component):void => {
+
+ this.loaderService.showLoader('composition-graph');
+
+ let onSuccess:(response:Models.RelationshipModel) => void = (relation:Models.RelationshipModel) => {
+ link.setRelation(relation);
+ this.commonGraphUtils.insertLinkToGraph(cy, link);
+ };
+
+ link.updateLinkDirection();
+
+ this.generalGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIActionWithReleaseCallback(
+ () => component.createRelation(link.relation).then(onSuccess),
+ () => this.loaderService.hideLoader('composition-graph')
+ );
+ };
+
+
+ public initScopeVls = ():void => {
+
+ let vls = this.leftPaletteLoaderService.getFullDataComponentList(Sdc.Utils.Constants.ResourceType.VL);
+ vls.forEach((item) => {
+ let key = _.find(Object.keys(item.capabilities), (key) => {
+ return _.includes(key.toLowerCase(), 'linkable');
+ });
+ let linkable = item.capabilities[key];
+ if (linkable) {
+ if (linkable[0].maxOccurrences == '2') {
+ this.p2pVL = _.find(vls, (component:Models.Components.Component) => {
+ return component.uniqueId === item.uniqueId;
+ });
+
+ } else {//assuming unbounded occurrences
+ this.mp2mpVL = _.find(vls, (component:Models.Components.Component) => {
+ return component.uniqueId === item.uniqueId;
+ });
+ }
+ }
+ });
+ };
+
+ private setVLlinks = (match:Models.MatchReqToReq, vl:Models.ComponentsInstances.ComponentInstance):Array<Models.RelationshipModel> => {
+
+ let relationship1 = new Models.Relationship();
+ let relationship2 = new Models.Relationship();
+ let newRelationshipModel1 = new Models.RelationshipModel();
+ let newRelationshipModel2 = new Models.RelationshipModel();
+
+ let capability:Models.Capability = vl.capabilities.findValueByKey('linkable')[0];
+ relationship1.setRelationProperties(capability, match.requirement);
+ relationship2.setRelationProperties(capability, match.secondRequirement);
+
+ newRelationshipModel1.setRelationshipModelParams(match.fromNode, vl.uniqueId, [relationship1]);
+ newRelationshipModel2.setRelationshipModelParams(match.toNode, vl.uniqueId, [relationship2]);
+
+ return [newRelationshipModel1, newRelationshipModel2];
+ };
+
+ private createVlinks = (cy:Cy.Instance, component:Models.Components.Component, matchReqToReq:Models.MatchReqToReq, vl:Models.Components.Component):void => {
+
+ let componentInstance:Models.ComponentsInstances.ComponentInstance = this.componentInstanceFactory.createComponentInstanceFromComponent(vl);
+ let fromNodePosition:Cy.Position = cy.getElementById(matchReqToReq.fromNode).relativePosition();
+ let toNodePosition:Cy.Position = cy.getElementById(matchReqToReq.toNode).relativePosition();
+ let location:Cy.Position = {
+ x: 0.5 * (fromNodePosition.x + toNodePosition.x),
+ y: 0.5 * (fromNodePosition.y + toNodePosition.y)
+ }
+
+ componentInstance.posX = location.x;
+ componentInstance.posY = location.y;
+
+ let onFailed:(error:any) => void = (error:any) => {
+ this.loaderService.hideLoader('composition-graph');
+ console.info('onFailed', error);
+ };
+
+ let onSuccess = (response:Models.ComponentsInstances.ComponentInstance):void => {
+
+ console.info('onSuccses', response);
+ response.requirements = new Models.RequirementsGroup(vl.requirements);
+ response.capabilities = new Models.CapabilitiesGroup(vl.capabilities);
+ response.componentVersion = vl.version;
+ response.setInstanceRC();
+
+ let newLinks = this.setVLlinks(matchReqToReq, response);
+ let newNode = this.nodesFactory.createNode(response);
+
+ this.commonGraphUtils.addComponentInstanceNodeToGraph(cy, newNode);
+
+ _.forEach(newLinks, (link) => {
+ let linkObg:Models.CompositionCiLinkBase = this.linksFactory.createGraphLink(cy, link, link.relationships[0]);
+ this.createLink(linkObg, cy, component);
+ });
+ };
+ component.createComponentInstance(componentInstance).then(onSuccess, onFailed);
+ };
+
+ private createSimpleLink = (match:Models.MatchReqToCapability, cy:Cy.Instance, component:Models.Components.Component):void => {
+ let newRelation:Models.RelationshipModel = match.matchToRelationModel();
+ let linkObg:Models.CompositionCiLinkBase = this.linksFactory.createGraphLink(cy,newRelation, newRelation.relationships[0]);
+ this.createLink(linkObg, cy, component);
+ };
+
+ public createLinkFromMenu = (cy:Cy.Instance, chosenMatch:Models.MatchBase, vl:Models.Components.Component, component:Models.Components.Component):void => {
+
+ if (chosenMatch) {
+ if (chosenMatch && chosenMatch instanceof Models.MatchReqToReq) {
+ this.createVlinks(cy, component, chosenMatch, vl); //TODO orit implement
+ }
+ if (chosenMatch && chosenMatch instanceof Models.MatchReqToCapability) {
+ this.createSimpleLink(chosenMatch, cy, component);
+ }
+ }
+ };
+
+
+ /**
+ * Filters the matches for UCPE links so that shown requirements and capabilites are only related to the selected ucpe-cp
+ * @param fromNode
+ * @param toNode
+ * @param matchesArray
+ * @returns {Array<Models.MatchBase>}
+ */
+ public filterUcpeLinks(fromNode: Models.Graph.CompositionCiNodeBase, toNode: Models.Graph.CompositionCiNodeBase, matchesArray: Array<Models.MatchBase>): any {
+
+ let matchLink: Array<Models.MatchBase>;
+
+ if (fromNode.isUcpePart) {
+ matchLink = _.filter(matchesArray, (match: Models.MatchBase) => {
+ return match.isOwner(fromNode.id);
+ });
+ }
+
+ if (toNode.isUcpePart) {
+ matchLink = _.filter(matchesArray, (match: Models.MatchBase) => {
+ return match.isOwner(toNode.id);
+ });
+ }
+ return matchLink ? matchLink : matchesArray;
+ }
+
+
+ /**
+ * open the connect link menu if the link drawn is valid - match requirements & capabilities
+ * @param cy
+ * @param fromNode
+ * @param toNode
+ * @returns {any}
+ */
+ public onLinkDrawn(cy:Cy.Instance, fromNode:Cy.CollectionFirstNode, toNode:Cy.CollectionFirstNode):Models.RelationMenuDirectiveObj {
+
+ if(!this.commonGraphUtils.nodeLocationsCompatible(cy, fromNode, toNode)){ return null; }
+ let linkModel:Array<Models.CompositionCiLinkBase> = this.generalGraphUtils.getAllCompositionCiLinks(cy);
+
+ let possibleRelations:Array<Models.MatchBase> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromNode.data().componentInstance,
+ toNode.data().componentInstance, linkModel, this.mp2mpVL); //TODO orit - add p2p and mp2mp
+
+ //filter relations found to limit to specific ucpe-cp
+ possibleRelations = this.filterUcpeLinks(fromNode.data(), toNode.data(), possibleRelations);
+
+ //if found possibleRelations between the nodes we create relation menu directive and open the link menu
+ if (possibleRelations.length) {
+ let menuPosition = this.generalGraphUtils.getLinkMenuPosition(cy, toNode.renderedPoint());
+ return new Models.RelationMenuDirectiveObj(fromNode.data(), toNode.data(), this.mp2mpVL, this.p2pVL, menuPosition, possibleRelations);
+ }
+ return null;
+ };
+
+
+ /**
+ * when we drag instance in to UCPE or out of UCPE - get all links we need to delete - one node in ucpe and one node outside of ucpe
+ * @param node - the node we dragged into or out of the ucpe
+ */
+ public deleteLinksWhenNodeMovedFromOrToUCPE(component:Models.Components.Component, cy:Cy.Instance, nodeMoved:Cy.CollectionNodes, vlsPendingDeletion?:Cy.CollectionNodes):void {
+
+
+ let linksToDelete:Cy.CollectionElements = cy.collection();
+ _.forEach(nodeMoved.neighborhood('node'), (neighborNode)=>{
+
+ if(neighborNode.data().isUcpePart){ //existing connections to ucpe or ucpe-cp - we want to delete even though nodeLocationsCompatible will technically return true
+ linksToDelete = linksToDelete.add(nodeMoved.edgesWith(neighborNode)); // This will delete the ucpe-host-link, or the vl-ucpe-link if nodeMoved is vl
+ } else if(!this.commonGraphUtils.nodeLocationsCompatible(cy, nodeMoved, neighborNode)){ //connection to regular node or vl - check if locations are compatible
+ if(!vlsPendingDeletion || !vlsPendingDeletion.intersect(neighborNode).length){ //Check if this is a link to a VL pending deletion, to prevent double deletion of between the node moved and vl
+ linksToDelete = linksToDelete.add(nodeMoved.edgesWith(neighborNode));
+ }
+ }
+ });
+
+
+
+ linksToDelete.each((i, link)=>{
+ this.deleteLink(cy, component, false, link);
+ });
+
+ };
+
+
+ /**
+ * Creates a hostedOn link between a VF and UCPE
+ * @param component
+ * @param cy
+ * @param ucpeNode
+ * @param vfNode
+ */
+ public createVfToUcpeLink = (component: Models.Components.Component, cy:Cy.Instance, ucpeNode:Models.Graph.NodeUcpe, vfNode:Models.Graph.CompositionCiNodeVf):void => {
+ let hostedOnMatch:Models.MatchReqToCapability = this.generalGraphUtils.canBeHostedOn(cy, ucpeNode.componentInstance, vfNode.componentInstance);
+ /* create relation */
+ let newRelation = new Models.RelationshipModel();
+ newRelation.fromNode = ucpeNode.id;
+ newRelation.toNode = vfNode.id;
+
+ let link:Models.CompositionCiLinkBase = this.linksFactory.createUcpeHostLink(newRelation);
+ link.relation = hostedOnMatch.matchToRelationModel();
+ this.createLink(link, cy, component);
+ };
+
+
+ /**
+ * Handles click event on links.
+ * If one edge selected: do nothing.
+ /*Two edges selected - always select all
+ /* Three or more edges: first click - select all, secondary click - select single.
+ * @param cy
+ * @param event
+ */
+ public handleLinkClick(cy:Cy.Instance, event : Cy.EventObject) {
+ if(cy.$('edge:selected').length > 2 && event.cyTarget[0].selected()) {
+ cy.$(':selected').unselect();
+ } else {
+
+ let vl: Cy.CollectionNodes = event.cyTarget[0].target('.vl-node');
+ let connectedEdges:Cy.CollectionEdges = vl.connectedEdges();
+ if (vl.length && connectedEdges.length > 1) {
+
+ setTimeout(() => {
+ vl.select();
+ connectedEdges.select();
+ }, 0);
+ }
+ }
+
+ }
+
+
+ /**
+ * Calculates the position for the menu that modifies an existing link
+ * @param event
+ * @param elementWidth
+ * @param elementHeight
+ * @returns {Sdc.Models.Graph.Point}
+ */
+ public calculateLinkMenuPosition(event, elementWidth, elementHeight): Sdc.Models.Graph.Point {
+ let point: Sdc.Models.Graph.Point = new Sdc.Models.Graph.Point(event.originalEvent.x,event.originalEvent.y);
+ if(event.originalEvent.view.screen.height-elementHeight<point.y){
+ point.y = event.originalEvent.view.screen.height-elementHeight;
+ }
+ if(event.originalEvent.view.screen.width-elementWidth<point.x){
+ point.x = event.originalEvent.view.screen.width-elementWidth;
+ }
+ return point;
+ };
+
+
+ /**
+ * Gets the menu that is displayed when you click an existing link.
+ * @param link
+ * @param event
+ * @returns {Models.LinkMenu}
+ */
+ public getModifyLinkMenu(link:Cy.CollectionFirstEdge, event:Cy.EventObject):Models.LinkMenu{
+ let point:Sdc.Models.Graph.Point = this.calculateLinkMenuPosition(event,Sdc.Utils.Constants.GraphUIObjects.MENU_LINK_VL_WIDTH_OFFSET,Sdc.Utils.Constants.GraphUIObjects.MENU_LINK_VL_HEIGHT_OFFSET);
+ let menu:Models.LinkMenu = new Models.LinkMenu(point, true, link);
+ return menu;
+ };
+
+ }
+
+
+
+ CompositionGraphLinkUtils.$inject = [
+ 'LinksFactory',
+ 'LoaderService',
+ 'CompositionGraphGeneralUtils',
+ 'LeftPaletteLoaderService',
+ 'ComponentInstanceFactory',
+ 'NodesFactory',
+ 'CommonGraphUtils',
+ 'MatchCapabilitiesRequirementsUtils'
+ ];
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-nodes-utils.ts b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-nodes-utils.ts
new file mode 100644
index 0000000000..95c31d16b1
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-nodes-utils.ts
@@ -0,0 +1,220 @@
+/**
+ * Created by obarda on 11/9/2016.
+ */
+
+/// <reference path="../../../../references"/>
+module Sdc.Graph.Utils {
+
+ export class CompositionGraphNodesUtils {
+ constructor(private NodesFactory:Sdc.Utils.NodesFactory, private $log:ng.ILogService,
+ private GeneralGraphUtils:Graph.Utils.CompositionGraphGeneralUtils,
+ private commonGraphUtils: Sdc.Graph.Utils.CommonGraphUtils,
+ private eventListenerService: Services.EventListenerService,
+ private loaderService:Services.LoaderService) {
+
+ }
+
+ /**
+ * Returns component instances for all nodes passed in
+ * @param nodes - Cy nodes
+ * @returns {any[]}
+ */
+ public getAllNodesData(nodes:Cy.CollectionNodes) {
+ return _.map(nodes, (node:Cy.CollectionFirstNode)=> {
+ return node.data();
+ })
+ };
+
+ /**
+ * Deletes component instances on server and then removes it from the graph as well
+ * @param cy
+ * @param component
+ * @param nodeToDelete
+ */
+ public deleteNode(cy: Cy.Instance, component:Models.Components.Component, nodeToDelete:Cy.CollectionNodes):void {
+
+ this.loaderService.showLoader('composition-graph');
+ let onSuccess:(response:Models.ComponentsInstances.ComponentInstance) => void = (response:Models.ComponentsInstances.ComponentInstance) => {
+ console.info('onSuccess', response);
+
+ //if node to delete is a UCPE, remove all children (except UCPE-CPs) and remove their "hostedOn" links
+ if (nodeToDelete.data().isUcpe){
+ _.each(cy.nodes('[?isInsideGroup]'), (node)=>{
+ this.eventListenerService.notifyObservers(Sdc.Utils.Constants.GRAPH_EVENTS.ON_REMOVE_NODE_FROM_UCPE, node, nodeToDelete);
+ });
+ }
+
+ //check whether the node is connected to any VLs that only have one other connection. If so, delete that VL as well
+ if(!(nodeToDelete.data() instanceof Sdc.Models.Graph.CompositionCiNodeVl)){
+ let connectedVls:Array<Cy.CollectionFirstNode> = this.getConnectedVlToNode(nodeToDelete);
+ this.handleConnectedVlsToDelete(connectedVls);
+ }
+
+ //update UI
+ cy.remove(nodeToDelete);
+
+ };
+
+ let onFailed:(response:any) => void = (response:any) => {
+ console.info('onFailed', response);
+ };
+
+
+ this.GeneralGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIActionWithReleaseCallback(
+ () => component.deleteComponentInstance(nodeToDelete.data().componentInstance.uniqueId).then(onSuccess, onFailed),
+ () => this.loaderService.hideLoader('composition-graph')
+ );
+
+ };
+
+
+ /**
+ * Finds all VLs connected to a single node
+ * @param node
+ * @returns {Array<Cy.CollectionFirstNode>}
+ */
+ public getConnectedVlToNode = (node: Cy.CollectionNodes): Array<Cy.CollectionFirstNode> => {
+ let connectedVls: Array<Cy.CollectionFirstNode> = new Array<Cy.CollectionFirstNode>();
+ _.forEach(node.connectedEdges().connectedNodes(), (node: Cy.CollectionFirstNode) => {
+ if (node.data() instanceof Models.Graph.CompositionCiNodeVl) {
+ connectedVls.push(node);
+ }
+ });
+ return connectedVls;
+ };
+
+
+ /**
+ * Delete all VLs that have only two connected nodes (this function is called when deleting a node)
+ * @param connectedVls
+ */
+ public handleConnectedVlsToDelete = (connectedVls: Array<Cy.CollectionFirstNode>) => {
+ _.forEach(connectedVls, (vlToDelete: Cy.CollectionNodes) => {
+
+ if (vlToDelete.connectedEdges().length === 2) { // if vl connected only to 2 nodes need to delete the vl
+ this.eventListenerService.notifyObservers(Sdc.Utils.Constants.GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, vlToDelete.data().componentInstance);
+ }
+ });
+ };
+
+
+ /**
+ * This function is called when moving a node in or out of UCPE.
+ * Deletes all connected VLs that have less than 2 valid connections remaining after the move
+ * Returns the collection of vls that are in the process of deletion (async) to prevent duplicate calls while deletion is in progress
+ * @param component
+ * @param cy
+ * @param node - node that was moved in/out of ucpe
+ */
+ public deleteNodeVLsUponMoveToOrFromUCPE = (component:Models.Components.Component, cy:Cy.Instance, node:Cy.CollectionNodes):Cy.CollectionNodes =>{
+ if(node.data() instanceof Models.Graph.CompositionCiNodeVl){ return;}
+
+ let connectedVLsToDelete:Cy.CollectionNodes = cy.collection();
+ _.forEach(node.neighborhood('node'), (connectedNode) => {
+
+ //Find all neighboring nodes that are VLs
+ if(connectedNode.data() instanceof Models.Graph.CompositionCiNodeVl){
+
+ //check VL's neighbors to see if it has 2 or more nodes whose location is compatible with VL (regardless of whether VL is in or out of UCPE)
+ let compatibleNodeCount = 0;
+ let vlNeighborhood = connectedNode.neighborhood('node');
+ _.forEach(vlNeighborhood, (vlNeighborNode)=>{
+ if(this.commonGraphUtils.nodeLocationsCompatible(cy, connectedNode, vlNeighborNode)) {
+ compatibleNodeCount ++;
+ }
+ });
+
+ if(compatibleNodeCount < 2) {
+ connectedVLsToDelete = connectedVLsToDelete.add(connectedNode);
+ }
+ }
+ });
+
+ connectedVLsToDelete.each((i, vlToDelete:Cy.CollectionNodes)=>{
+ this.deleteNode(cy, component, vlToDelete);
+ });
+ return connectedVLsToDelete;
+ };
+
+ /**
+ * This function will update nodes position. if the new position is into or out of ucpe, the node will trigger the ucpe events
+ * @param cy
+ * @param component
+ * @param nodesMoved - the node/multiple nodes now moved by the user
+ */
+ public onNodesPositionChanged = (cy: Cy.Instance, component:Models.Components.Component, nodesMoved: Cy.CollectionNodes): void => {
+
+ if (nodesMoved.length === 0) {
+ return;
+ }
+
+ let isValidMove:boolean = this.GeneralGraphUtils.isGroupValidDrop(cy, nodesMoved);
+ if (isValidMove) {
+
+ this.$log.debug(`composition-graph::ValidDrop:: updating node position`);
+ let instancesToUpdateInNonBlockingAction:Array<Models.ComponentsInstances.ComponentInstance> = new Array<Models.ComponentsInstances.ComponentInstance>();
+
+ _.each(nodesMoved, (node:Cy.CollectionFirstNode)=> { //update all nodes new position
+
+ if(node.data().isUcpePart && !node.data().isUcpe){ return; }//No need to update UCPE-CPs
+
+ //update position
+ let newPosition:Cy.Position = this.commonGraphUtils.getNodePosition(node);
+ node.data().componentInstance.updatePosition(newPosition.x, newPosition.y);
+
+ //check if node moved to or from UCPE
+ let ucpe = this.commonGraphUtils.isInUcpe(node.cy(), node.boundingbox());
+ if(node.data().isInsideGroup || ucpe.length) {
+ this.handleUcpeChildMove(node, ucpe, instancesToUpdateInNonBlockingAction);
+ } else {
+ instancesToUpdateInNonBlockingAction.push(node.data().componentInstance);
+ }
+
+ });
+
+ if (instancesToUpdateInNonBlockingAction.length > 0) {
+ this.GeneralGraphUtils.pushMultipleUpdateComponentInstancesRequestToQueue(false, instancesToUpdateInNonBlockingAction, component);
+ }
+ } else {
+ this.$log.debug(`composition-graph::notValidDrop:: node return to latest position`);
+ //reset nodes position
+ nodesMoved.positions((i, node) => {
+ return {
+ x: +node.data().componentInstance.posX,
+ y: +node.data().componentInstance.posY
+ };
+ })
+ }
+
+ this.GeneralGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIActionWithReleaseCallback(() => {
+ }, () => {
+ this.loaderService.hideLoader('composition-graph');
+ });
+
+ };
+
+ /**
+ * Checks whether the node has been added or removed from UCPE and triggers appropriate events
+ * @param node - node moved
+ * @param ucpeContainer - UCPE container that the node has been moved to. When moving a node out of ucpe, param will be empty
+ * @param instancesToUpdateInNonBlockingAction
+ */
+ public handleUcpeChildMove(node:Cy.CollectionFirstNode, ucpeContainer:Cy.CollectionElements, instancesToUpdateInNonBlockingAction:Array<Models.ComponentsInstances.ComponentInstance>){
+
+ if(node.data().isInsideGroup){
+ if(ucpeContainer.length){ //moving node within UCPE. Simply update position
+ this.commonGraphUtils.updateUcpeChildPosition(<Cy.CollectionNodes>node, ucpeContainer);
+ instancesToUpdateInNonBlockingAction.push(node.data().componentInstance);
+ } else { //removing node from UCPE. Notify observers
+ this.eventListenerService.notifyObservers(Sdc.Utils.Constants.GRAPH_EVENTS.ON_REMOVE_NODE_FROM_UCPE, node, ucpeContainer);
+ }
+ } else if(!node.data().isInsideGroup && ucpeContainer.length && !node.data().isUcpePart){ //adding node to UCPE
+ this.eventListenerService.notifyObservers(Sdc.Utils.Constants.GRAPH_EVENTS.ON_INSERT_NODE_TO_UCPE, node, ucpeContainer, true);
+ }
+ }
+
+ }
+
+
+ CompositionGraphNodesUtils.$inject = ['NodesFactory', '$log', 'CompositionGraphGeneralUtils', 'CommonGraphUtils', 'EventListenerService', 'LoaderService'];
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/match-capability-requierment-utils.ts b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/match-capability-requierment-utils.ts
new file mode 100644
index 0000000000..5a401df317
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/match-capability-requierment-utils.ts
@@ -0,0 +1,265 @@
+/**
+ * Created by obarda on 1/1/2017.
+ */
+/// <reference path="../../../../references"/>
+module Sdc.Graph.Utils {
+
+ export class MatchCapabilitiesRequirementsUtils {
+
+ constructor() {
+ }
+
+
+
+ public static linkable(requirement1:Models.Requirement, requirement2:Models.Requirement, vlCapability:Models.Capability):boolean {
+ return MatchCapabilitiesRequirementsUtils.isMatch(requirement1, vlCapability) && MatchCapabilitiesRequirementsUtils.isMatch(requirement2, vlCapability);
+ };
+
+
+ /**
+ * Shows + icon in corner of each node passed in
+ * @param filteredNodesData
+ * @param cy
+ */
+ public highlightMatchingComponents(filteredNodesData, cy:Cy.Instance) {
+ _.each(filteredNodesData, (data:any) => {
+ let node = cy.getElementById(data.id);
+ cy.emit('showhandle', [node]);
+ });
+ }
+
+ /**
+ * Adds opacity to each node that cannot be linked to hovered node
+ * @param filteredNodesData
+ * @param nodesData
+ * @param cy
+ * @param hoveredNodeData
+ */
+ public fadeNonMachingComponents(filteredNodesData, nodesData, cy:Cy.Instance, hoveredNodeData?) {
+ let fadeNodes = _.xorWith(nodesData, filteredNodesData, (node1, node2) => {
+ return node1.id === node2.id;
+ });
+ if (hoveredNodeData) {
+ _.remove(fadeNodes, hoveredNodeData);
+ }
+ cy.batch(()=> {
+ _.each(fadeNodes, (node) => {
+ cy.getElementById(node.id).style({'background-image-opacity': 0.4});
+ });
+ })
+ }
+
+ /**
+ * Resets all nodes to regular opacity
+ * @param cy
+ */
+ public resetFadedNodes(cy:Cy.Instance) {
+ cy.batch(()=> {
+ cy.nodes().style({'background-image-opacity': 1});
+ })
+ }
+
+ // -------------------------------------------ALL FUNCTIONS NEED REFACTORING---------------------------------------------------------------//
+
+ private static requirementFulfilled(fromNodeId:string, requirement:any, links:Array<Models.CompositionCiLinkBase>):boolean {
+ return _.some(links, {
+ 'relation': {
+ 'fromNode': fromNodeId,
+ 'relationships': [{
+ 'requirementOwnerId': requirement.ownerId,
+ 'requirement': requirement.name,
+ 'relationship': {
+ 'type': requirement.relationship
+ }
+ }
+ ]
+ }
+ });
+ };
+
+ private static isMatch(requirement:Models.Requirement, capability:Models.Capability):boolean {
+ if (capability.type === requirement.capability) {
+ if (requirement.node) {
+ if (_.includes(capability.capabilitySources, requirement.node)) {
+ return true;
+ }
+ } else {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ private getFromToMatches(requirements1:Models.RequirementsGroup,
+ requirements2:Models.RequirementsGroup,
+ capabilities:Models.CapabilitiesGroup,
+ links:Array<Models.CompositionCiLinkBase>,
+ fromId:string,
+ toId:string,
+ vlCapability?:Models.Capability):Array<Models.MatchBase> {
+ let matches:Array<Models.MatchBase> = new Array<Models.MatchBase>();
+ _.forEach(requirements1, (requirementValue:Array<Models.Requirement>, key) => {
+ _.forEach(requirementValue, (requirement:Models.Requirement) => {
+ if (requirement.name !== "dependency" && !MatchCapabilitiesRequirementsUtils.requirementFulfilled(fromId, requirement, links)) {
+ _.forEach(capabilities, (capabilityValue:Array<Models.Capability>, key) => {
+ _.forEach(capabilityValue, (capability:Models.Capability) => {
+ if (MatchCapabilitiesRequirementsUtils.isMatch(requirement, capability)) {
+ let match:Models.MatchReqToCapability = new Models.MatchReqToCapability(requirement, capability, true, fromId, toId);
+ matches.push(match);
+ }
+ });
+ });
+ if (vlCapability) {
+ _.forEach(requirements2, (requirement2Value:Array<Models.Requirement>, key) => {
+ _.forEach(requirement2Value, (requirement2:Models.Requirement) => {
+ if (!MatchCapabilitiesRequirementsUtils.requirementFulfilled(toId, requirement2, links) && MatchCapabilitiesRequirementsUtils.linkable(requirement, requirement2, vlCapability)) {
+ let match:Models.MatchReqToReq = new Models.MatchReqToReq(requirement, requirement2, true, fromId, toId);
+ matches.push(match);
+ }
+ });
+ });
+ }
+ }
+ });
+ });
+ return matches;
+ }
+
+ private getToFromMatches(requirements:Models.RequirementsGroup, capabilities:Models.CapabilitiesGroup, links:Array<Models.CompositionCiLinkBase>, fromId:string, toId:string):Array<Models.MatchReqToCapability> {
+ let matches:Array<Models.MatchReqToCapability> = [];
+ _.forEach(requirements, (requirementValue:Array<Models.Requirement>, key) => {
+ _.forEach(requirementValue, (requirement:Models.Requirement) => {
+ if (requirement.name !== "dependency" && !MatchCapabilitiesRequirementsUtils.requirementFulfilled(toId, requirement, links)) {
+ _.forEach(capabilities, (capabilityValue:Array<Models.Capability>, key) => {
+ _.forEach(capabilityValue, (capability:Models.Capability) => {
+ if (MatchCapabilitiesRequirementsUtils.isMatch(requirement, capability)) {
+ let match:Models.MatchReqToCapability = new Models.MatchReqToCapability(requirement, capability, false, toId, fromId);
+ matches.push(match);
+ }
+ });
+ });
+ }
+ });
+ });
+ return matches;
+ }
+
+ public getMatchedRequirementsCapabilities(fromComponentInstance:Models.ComponentsInstances.ComponentInstance,
+ toComponentInstance:Models.ComponentsInstances.ComponentInstance,
+ links:Array<Models.CompositionCiLinkBase>,
+ vl?:Models.Components.Component):Array<Models.MatchBase> {//TODO allow for VL array
+ let linkCapability;
+ if (vl) {
+ let linkCapabilities:Array<Models.Capability> = vl.capabilities.findValueByKey('linkable');
+ if (linkCapabilities) {
+ linkCapability = linkCapabilities[0];
+ }
+ }
+ let fromToMatches:Array<Models.MatchBase> = this.getFromToMatches(fromComponentInstance.requirements,
+ toComponentInstance.requirements,
+ toComponentInstance.capabilities,
+ links,
+ fromComponentInstance.uniqueId,
+ toComponentInstance.uniqueId,
+ linkCapability);
+ let toFromMatches:Array<Models.MatchReqToCapability> = this.getToFromMatches(toComponentInstance.requirements,
+ fromComponentInstance.capabilities,
+ links,
+ fromComponentInstance.uniqueId,
+ toComponentInstance.uniqueId);
+
+ return fromToMatches.concat(toFromMatches);
+ }
+
+
+
+
+
+ /**
+ * Step I: Check if capabilities of component match requirements of nodeDataArray
+ * 1. Get component capabilities and loop on each capability
+ * 2. Inside the loop, perform another loop on all nodeDataArray, and fetch the requirements for each one
+ * 3. Loop on the requirements, and verify match (see in code the rules)
+ *
+ * Step II: Check if requirements of component match capabilities of nodeDataArray
+ * 1. Get component requirements and loop on each requirement
+ * 2.
+ *
+ * @param component - this is the hovered resource of the left panel of composition screen
+ * @param nodeDataArray - Array of resource instances that are on the canvas
+ * @param links -getMatchedRequirementsCapabilities
+ * @param vl -
+ * @returns {any[]|T[]}
+ */
+ public findByMatchingCapabilitiesToRequirements(component:Models.Components.Component,
+ nodeDataArray:Array<Models.Graph.CompositionCiNodeBase>,
+ links:Array<Models.CompositionCiLinkBase>,
+ vl?:Models.Components.Component):Array<any> {//TODO allow for VL array
+ let res = [];
+
+ // STEP I
+ {
+ let capabilities:any = component.capabilities;
+ _.forEach(capabilities, (capabilityValue:Array<any>, capabilityKey)=> {
+ _.forEach(capabilityValue, (capability)=> {
+ _.forEach(nodeDataArray, (node:Models.Graph.CompositionCiNodeBase)=> {
+ if (node && node.componentInstance) {
+ let requirements:any = node.componentInstance.requirements;
+ let fromNodeId:string = node.componentInstance.uniqueId;
+ _.forEach(requirements, (requirementValue:Array<any>, requirementKey)=> {
+ _.forEach(requirementValue, (requirement)=> {
+ if (requirement.name !== "dependency" && MatchCapabilitiesRequirementsUtils.isMatch(requirement, capability)
+ && !MatchCapabilitiesRequirementsUtils.requirementFulfilled(fromNodeId, requirement, links)) {
+ res.push(node);
+ }
+ });
+ });
+ }
+ });
+ });
+ });
+ }
+
+ // STEP II
+ {
+ let requirements:any = component.requirements;
+ let fromNodeId:string = component.uniqueId;
+ let linkCapability:Array<Models.Capability> = vl ? vl.capabilities.findValueByKey('linkable') : undefined;
+
+ _.forEach(requirements, (requirementValue:Array<any>, requirementKey)=> {
+ _.forEach(requirementValue, (requirement)=> {
+ if (requirement.name !== "dependency" && !MatchCapabilitiesRequirementsUtils.requirementFulfilled(fromNodeId, requirement, links)) {
+ _.forEach(nodeDataArray, (node:any)=> {
+ if (node && node.componentInstance && node.category !== 'groupCp') {
+ let capabilities:any = node.componentInstance.capabilities;
+ _.forEach(capabilities, (capabilityValue:Array<any>, capabilityKey)=> {
+ _.forEach(capabilityValue, (capability)=> {
+ if (MatchCapabilitiesRequirementsUtils.isMatch(requirement, capability)) {
+ res.push(node);
+ }
+ });
+ });
+ if (linkCapability) {
+ let linkRequirements = node.componentInstance.requirements;
+ _.forEach(linkRequirements, (value:Array<any>, key)=> {
+ _.forEach(value, (linkRequirement)=> {
+ if (!MatchCapabilitiesRequirementsUtils.requirementFulfilled(node.componentInstance.uniqueId, linkRequirement, links)
+ && MatchCapabilitiesRequirementsUtils.linkable(requirement, linkRequirement, linkCapability[0])) {
+ res.push(node);
+ }
+ });
+ });
+ }
+ }
+ });
+ }
+ });
+ });
+ }
+
+ return _.uniq(res);
+ };
+ }
+
+ MatchCapabilitiesRequirementsUtils.$inject = [];
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.directive.ts b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.directive.ts
new file mode 100644
index 0000000000..d6d4aef374
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.directive.ts
@@ -0,0 +1,114 @@
+/**
+ * Created by obarda on 12/19/2016.
+ */
+/// <reference path="../../../references"/>
+module Sdc.Directives {
+
+ import Util = jasmine.Util;
+
+ interface IDeploymentGraphScope extends ng.IScope {
+ component:Models.Components.Component;
+ }
+
+ export class DeploymentGraph implements ng.IDirective {
+ private _cy:Cy.Instance;
+
+ constructor(private NodesFactory:Utils.NodesFactory, private commonGraphUtils:Graph.Utils.CommonGraphUtils,
+ private deploymentGraphGeneralUtils:Graph.Utils.DeploymentGraphGeneralUtils, private ComponentInstanceFactory: Sdc.Utils.ComponentInstanceFactory) {
+ }
+
+ restrict = 'E';
+ templateUrl = '/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.html';
+ scope = {
+ component: '=',
+ isViewOnly: '='
+ };
+
+ link = (scope:IDeploymentGraphScope, el:JQuery) => {
+ if(scope.component.isResource()) {
+ this.loadGraph(scope, el);
+ this.registerGraphEvents();
+ }
+ };
+
+
+ public initGraphNodes = (cy:Cy.Instance, component:Models.Components.Component):void => {
+ if (component.groups) { // Init module nodes
+ _.each(component.groups, (groupModule:Models.Module) => {
+ let moduleNode = this.NodesFactory.createModuleNode(groupModule);
+ this.commonGraphUtils.addNodeToGraph(cy, moduleNode);
+
+ });
+ }
+ _.each(component.componentInstances, (instance:Models.ComponentsInstances.ComponentInstance) => { // Init component instance nodes
+ let componentInstanceNode = this.NodesFactory.createNode(instance);
+ componentInstanceNode.parent = this.deploymentGraphGeneralUtils.findInstanceModule(component.groups, instance.uniqueId);
+ if (componentInstanceNode.parent) { // we are not drawing instances that are not a part of a module
+ this.commonGraphUtils.addComponentInstanceNodeToGraph(cy, componentInstanceNode);
+ }
+ });
+
+ // This is a special functionality to pass the cytoscape default behavior - we can't create Parent module node without children's
+ // so we must add an empty dummy child node
+ _.each(this._cy.nodes('[?isGroup]'), (moduleNode: Cy.CollectionFirstNode) => {
+ if (!moduleNode.isParent()) {
+ let dummyInstance = this.ComponentInstanceFactory.createEmptyComponentInstance();
+ let componentInstanceNode = this.NodesFactory.createNode(dummyInstance);
+ componentInstanceNode.parent = moduleNode.id();
+ let dummyNode = this.commonGraphUtils.addNodeToGraph(cy, componentInstanceNode, moduleNode.position());
+ dummyNode.addClass('dummy-node');
+ }
+ })
+ };
+
+ private registerGraphEvents() {
+
+ this._cy.on('afterExpand', (event) => {
+ event.cyTarget.qtip({});
+ });
+
+ this._cy.on('afterCollapse', (event) => {
+ this.commonGraphUtils.initNodeTooltip(event.cyTarget);
+ });
+ }
+
+ private loadGraph = (scope:IDeploymentGraphScope, el:JQuery) => {
+
+ let graphEl = el.find('.sdc-deployment-graph-wrapper');
+ this._cy = cytoscape({
+ container: graphEl,
+ style: Sdc.Graph.Utils.ComponentIntanceNodesStyle.getCompositionGraphStyle().concat(Sdc.Graph.Utils.ModulesNodesStyle.getModuleGraphStyle()),
+ zoomingEnabled: false,
+ selectionType: 'single',
+
+ });
+
+ //adding expand collapse extension
+ this._cy.expandCollapse({
+ layoutBy: {
+ name: "grid",
+ animate: true,
+ randomize: false,
+ fit: true
+ },
+ fisheye: false,
+ undoable: false,
+ expandCollapseCueSize: 18,
+ expandCueImage: Sdc.Utils.Constants.IMAGE_PATH + '/styles/images/resource-icons/' + 'closeModule.png',
+ collapseCueImage: Sdc.Utils.Constants.IMAGE_PATH + '/styles/images/resource-icons/' + 'openModule.png',
+ expandCollapseCueSensitivity: 2,
+ cueOffset: -20
+ });
+
+ this.initGraphNodes(this._cy, scope.component); //creating instances nodes
+ this.commonGraphUtils.initGraphLinks(this._cy, scope.component.componentInstancesRelations);
+ this._cy.collapseAll();
+ };
+
+ public static factory = (NodesFactory:Utils.NodesFactory, CommonGraphUtils:Graph.Utils.CommonGraphUtils, DeploymentGraphGeneralUtils:Graph.Utils.DeploymentGraphGeneralUtils, ComponentInstanceFactory: Utils.ComponentInstanceFactory) => {
+ return new DeploymentGraph(NodesFactory, CommonGraphUtils, DeploymentGraphGeneralUtils, ComponentInstanceFactory)
+ }
+ }
+
+ DeploymentGraph.factory.$inject = ['NodesFactory', 'CommonGraphUtils', 'DeploymentGraphGeneralUtils', 'ComponentInstanceFactory'];
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.html b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.html
new file mode 100644
index 0000000000..55e1c131f4
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.html
@@ -0,0 +1,2 @@
+<div class="sdc-deployment-graph-wrapper" ng-class="{'view-only':isViewOnly}">
+</div> \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.less b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.less
new file mode 100644
index 0000000000..ff8fc46380
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.less
@@ -0,0 +1,14 @@
+deployment-graph {
+ display: block;
+ height:100%;
+ width: 100%;
+
+ .sdc-deployment-graph-wrapper {
+ height:100%;
+ width: 100%;
+ }
+
+ .view-only{
+ background-color:rgb(248, 248, 248);
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-utils/deployment-graph-general-utils.ts b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-utils/deployment-graph-general-utils.ts
new file mode 100644
index 0000000000..3ad9da56be
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-utils/deployment-graph-general-utils.ts
@@ -0,0 +1,24 @@
+/**
+ * Created by obarda on 12/21/2016.
+ */
+/// <reference path="../../../../references"/>
+module Sdc.Graph.Utils {
+
+ export class DeploymentGraphGeneralUtils {
+
+ constructor() {
+
+ }
+
+ public findInstanceModule = (groupsArray:Array<Models.Module>, componentInstanceId:string):string => {
+ let parentGroup:Sdc.Models.Module = _.find(groupsArray, (group:Sdc.Models.Module) => {
+ return _.find(group.members, (member) => {
+ return member === componentInstanceId;
+ });
+ });
+ return parentGroup ? parentGroup.uniqueId : "";
+ };
+ }
+
+ DeploymentGraphGeneralUtils.$inject = [];
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/image-creator/image-creator.service.ts b/catalog-ui/app/scripts/directives/graphs-v2/image-creator/image-creator.service.ts
new file mode 100644
index 0000000000..e3b17e163d
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/image-creator/image-creator.service.ts
@@ -0,0 +1,46 @@
+module Sdc.Utils {
+ export class ImageCreatorService {
+ static '$inject' = ['$q'];
+ private _canvas: HTMLCanvasElement;
+
+ constructor(private $q: ng.IQService) {
+ this._canvas = <HTMLCanvasElement>$('<canvas>')[0];
+ this._canvas.setAttribute('style', 'display:none');
+
+ let body = document.getElementsByTagName('body')[0];
+ body.appendChild(this._canvas);
+ }
+
+ getImageBase64(imageBaseUri: string, imageLayerUri: string): ng.IPromise<string> {
+ let deferred = this.$q.defer();
+ let imageBase = new Image();
+ let imageLayer = new Image();
+ let imagesLoaded = 0;
+ let onImageLoaded = () => {
+ imagesLoaded++;
+
+ if (imagesLoaded < 2) {
+ return;
+ }
+ this._canvas.setAttribute('width', imageBase.width.toString());
+ this._canvas.setAttribute('height', imageBase.height.toString());
+
+ let canvasCtx = this._canvas.getContext('2d');
+ canvasCtx.clearRect(0, 0, this._canvas.width, this._canvas.height);
+
+ canvasCtx.drawImage(imageBase, 0, 0, imageBase.width, imageBase.height);
+ canvasCtx.drawImage(imageLayer, imageBase.width - imageLayer.width, 0, imageLayer.width, imageLayer.height);
+
+ let base64Image = this._canvas.toDataURL();
+ deferred.resolve(base64Image);
+ };
+
+ imageBase.onload = onImageLoaded;
+ imageLayer.onload = onImageLoaded;
+ imageBase.src = imageBaseUri;
+ imageLayer.src = imageLayerUri;
+
+ return deferred.promise;
+ }
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/palette/interfaces/i-dragdrop-event.d.ts b/catalog-ui/app/scripts/directives/graphs-v2/palette/interfaces/i-dragdrop-event.d.ts
new file mode 100644
index 0000000000..26c042611c
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/palette/interfaces/i-dragdrop-event.d.ts
@@ -0,0 +1,7 @@
+interface IDragDropEvent extends JQueryEventObject {
+ dataTransfer: any;
+ toElement: {
+ naturalWidth: number;
+ naturalHeight: number;
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.directive.ts b/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.directive.ts
new file mode 100644
index 0000000000..c00da6d1df
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.directive.ts
@@ -0,0 +1,327 @@
+/// <reference path="../../../references"/>
+
+module Sdc.Directives {
+ import Dictionary = Sdc.Utils.Dictionary;
+ import GRAPH_EVENTS = Sdc.Utils.Constants.GRAPH_EVENTS;
+ import ImageCreatorService = Sdc.Utils.ImageCreatorService;
+ interface IPaletteScope {
+ components: any;
+ currentComponent: any;
+ model: any;
+ displaySortedCategories: any;
+ expandedSection: string;
+
+ p2pVL: Models.Components.Component;
+ mp2mpVL: Models.Components.Component;
+ vlType: string;
+ dragElement: JQuery;
+ dragbleNode: {
+ event: JQueryEventObject,
+ components: Models.DisplayComponent,
+ ui: any
+ }
+
+ sectionClick: (section: string)=>void;
+ searchComponents: (searchText: string)=>void;
+ onMouseOver: (displayComponent: Models.DisplayComponent)=>void;
+ onMouseOut: (displayComponent: Models.DisplayComponent)=>void;
+ dragStartCallback: (event: JQueryEventObject, ui, displayComponent: Models.DisplayComponent)=>void;
+ dragStopCallback: ()=>void;
+ onDragCallback: (event:JQueryEventObject) => void;
+ setElementTemplate: (e: JQueryEventObject)=>void;
+
+ isOnDrag: boolean;
+ isDragable: boolean;
+ isLoading: boolean;
+ isViewOnly: boolean;
+ }
+
+ export class Palette implements ng.IDirective {
+ constructor(private $log: ng.ILogService,
+ private LeftPaletteLoaderService,
+ private sdcConfig,
+ private ComponentFactory,
+ private ComponentInstanceFactory: Utils.ComponentInstanceFactory,
+ private NodesFactory: Utils.NodesFactory,
+ private CompositionGraphGeneralUtils: Graph.Utils.CompositionGraphGeneralUtils,
+ private EventListenerService: Services.EventListenerService,
+ private sdcMenu: Models.IAppMenu) {
+
+ }
+
+ private fetchingComponentFromServer: boolean = false;
+ private nodeHtmlSubstitute: JQuery;
+
+ scope = {
+ components: '=',
+ currentComponent: '=',
+ isViewOnly: '=',
+ isLoading: '='
+ };
+ restrict = 'E';
+ templateUrl = '/app/scripts/directives/graphs-v2/palette/palette.html';
+
+ link = (scope: IPaletteScope, el: JQuery) => {
+ this.nodeHtmlSubstitute = $('<div class="node-substitute"><span></span><img /></div>');
+ el.append(this.nodeHtmlSubstitute);
+
+ this.initComponents(scope);
+ this.initScopeVls(scope);
+ this.initEvents(scope);
+ this.initDragEvents(scope);
+ this._initExpandedSection(scope, '');
+ };
+
+ private leftPanelResourceFilter(resourcesNotAbstract: Array<Models.DisplayComponent>, resourceFilterTypes: Array<string>): Array<Models.DisplayComponent> {
+ let filterResources = _.filter(resourcesNotAbstract, (component) => {
+ return resourceFilterTypes.indexOf(component.getComponentSubType()) > -1;
+ });
+ return filterResources;
+ }
+
+ private initLeftPanel(leftPanelComponents: Array<Models.DisplayComponent>, resourceFilterTypes: Array<string>): Models.LeftPanelModel {
+ let leftPanelModel = new Models.LeftPanelModel();
+
+ if (resourceFilterTypes && resourceFilterTypes.length) {
+ leftPanelComponents = this.leftPanelResourceFilter(leftPanelComponents, resourceFilterTypes);
+ }
+ leftPanelModel.numberOfElements = leftPanelComponents && leftPanelComponents.length || 0;
+
+ if (leftPanelComponents && leftPanelComponents.length) {
+
+ let categories: any = _.groupBy(leftPanelComponents, 'mainCategory');
+ for (let category in categories)
+ categories[category] = _.groupBy(categories[category], 'subCategory');
+
+ leftPanelModel.sortedCategories = categories;
+ }
+ return leftPanelModel;
+ }
+
+ private initScopeVls(scope: IPaletteScope): void {
+ let vls = this.LeftPaletteLoaderService.getFullDataComponentList(Utils.Constants.ResourceType.VL);
+ scope.vlType = null;
+ vls.forEach((item) => {
+ let key = _.find(Object.keys(item.capabilities), (key) => {
+ return _.includes(key.toLowerCase(), 'linkable');
+ });
+ let linkable = item.capabilities[key];
+ if (linkable) {
+ if (linkable[0].maxOccurrences == '2') {
+ scope.p2pVL = _.find(vls, (component: Models.Components.Component) => {
+ return component.uniqueId === item.uniqueId;
+ });
+
+ } else {//assuming unbounded occurrences
+ scope.mp2mpVL = _.find(vls, (component: Models.Components.Component) => {
+ return component.uniqueId === item.uniqueId;
+ });
+ }
+ }
+ });
+ };
+
+ private initEvents(scope: IPaletteScope) {
+ /**
+ *
+ * @param section
+ */
+ scope.sectionClick = (section: string) => {
+ if (section === scope.expandedSection) {
+ scope.expandedSection = '';
+ return;
+ }
+ scope.expandedSection = section;
+ };
+
+ scope.onMouseOver = (displayComponent: Models.DisplayComponent) => {
+ if (scope.isOnDrag) {
+ return;
+ }
+ scope.isOnDrag = true;
+
+ this.EventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, displayComponent);
+ this.$log.debug('palette::onMouseOver:: fired');
+
+ if (this.CompositionGraphGeneralUtils.componentRequirementsAndCapabilitiesCaching.containsKey(displayComponent.uniqueId)) {
+ this.$log.debug(`palette::onMouseOver:: component id ${displayComponent.uniqueId} found in cache`);
+ let cacheComponent: Models.Components.Component = this.CompositionGraphGeneralUtils.componentRequirementsAndCapabilitiesCaching.getValue(displayComponent.uniqueId);
+
+ //TODO: Danny: fire event to highlight matching nodes
+ //showMatchingNodes(cacheComponent);
+ return;
+ }
+
+ this.$log.debug(`palette::onMouseOver:: component id ${displayComponent.uniqueId} not found in cache, initiating server get`);
+ // This will bring the component from the server including requirements and capabilities
+ // Check that we do not fetch many times, because only in the success we add the component to componentRequirementsAndCapabilitiesCaching
+ if (this.fetchingComponentFromServer) {
+ return;
+ }
+
+ this.fetchingComponentFromServer = true;
+ this.ComponentFactory.getComponentFromServer(displayComponent.componentSubType, displayComponent.uniqueId)
+ .then((component: Models.Components.Component) => {
+ this.$log.debug(`palette::onMouseOver:: component id ${displayComponent.uniqueId} fetch success`);
+ this.LeftPaletteLoaderService.updateSpecificComponentLeftPalette(component, scope.currentComponent.componentType);
+ this.CompositionGraphGeneralUtils.componentRequirementsAndCapabilitiesCaching.setValue(component.uniqueId, component);
+ this.fetchingComponentFromServer = false;
+
+ //TODO: Danny: fire event to highlight matching nodes
+ //showMatchingNodes(component);
+ })
+ .catch(() => {
+ this.$log.debug('palette::onMouseOver:: component id fetch error');
+ this.fetchingComponentFromServer = false;
+ });
+
+
+ };
+
+ scope.onMouseOut = () => {
+ scope.isOnDrag = false;
+ this.EventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT);
+ }
+ }
+
+ private initComponents(scope: IPaletteScope) {
+ scope.searchComponents = (searchText: any): void => {
+ scope.displaySortedCategories = this._searchComponents(searchText, scope.model.sortedCategories);
+ this._initExpandedSection(scope, searchText);
+ };
+
+ scope.isDragable = scope.currentComponent.isComplex();
+ let entityType: string = scope.currentComponent.componentType.toLowerCase();
+ let resourceFilterTypes: Array<string> = this.sdcConfig.resourceTypesFilter[entityType];
+
+ scope.components = this.LeftPaletteLoaderService.getLeftPanelComponentsForDisplay(scope.currentComponent.componentType);
+ scope.model = this.initLeftPanel(scope.components, resourceFilterTypes);
+ scope.displaySortedCategories = angular.copy(scope.model.sortedCategories);
+ }
+
+ private _initExpandedSection(scope: IPaletteScope, searchText: string): void {
+ if (searchText == '') {
+ let isContainingCategory: boolean = false;
+ let categoryToExpand: string;
+ if (scope.currentComponent && scope.currentComponent.categories && scope.currentComponent.categories[0]) {
+ categoryToExpand = this.sdcMenu.categoriesDictionary[scope.currentComponent.categories[0].name];
+ for (let category in scope.model.sortedCategories) {
+ if (categoryToExpand == category) {
+ isContainingCategory = true;
+ break;
+ }
+ }
+ }
+ isContainingCategory ? scope.expandedSection = categoryToExpand : scope.expandedSection = 'Generic';
+ }
+ else {
+ scope.expandedSection = Object.keys(scope.displaySortedCategories).sort()[0];
+ }
+ };
+
+ private initDragEvents(scope: IPaletteScope) {
+ scope.dragStartCallback = (event: IDragDropEvent, ui, displayComponent: Models.DisplayComponent): void => {
+ if (scope.isLoading || !scope.isDragable || scope.isViewOnly) {
+ return;
+ }
+
+ let component = _.find(this.LeftPaletteLoaderService.getFullDataComponentListWithVls(scope.currentComponent.componentType), (componentFullData: Models.DisplayComponent) => {
+ return displayComponent.uniqueId === componentFullData.uniqueId;
+ });
+ this.EventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_START, scope.dragElement, component);
+
+ scope.isOnDrag = true;
+
+
+
+ // this.graphUtils.showMatchingNodes(component, myDiagram, scope.sdcConfig.imagesPath);
+ // document.addEventListener('mousemove', moveOnDocument);
+ event.dataTransfer.component = component;
+ };
+
+ scope.dragStopCallback = () => {
+ scope.isOnDrag = false;
+ };
+
+ scope.onDragCallback = (event:IDragDropEvent): void => {
+ this.EventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_ACTION, event);
+ };
+ scope.setElementTemplate = (e) => {
+ let dragComponent: Models.Components.Component = _.find(this.LeftPaletteLoaderService.getFullDataComponentListWithVls(scope.currentComponent.componentType),
+ (fullComponent: Models.Components.Component) => {
+ return (<any>angular.element(e.currentTarget).scope()).component.uniqueId === fullComponent.uniqueId;
+ });
+ let componentInstance: Models.ComponentsInstances.ComponentInstance = this.ComponentInstanceFactory.createComponentInstanceFromComponent(dragComponent);
+ let node: Models.Graph.CompositionCiNodeBase = this.NodesFactory.createNode(componentInstance);
+
+ // myDiagram.dragFromPalette = node;
+ this.nodeHtmlSubstitute.find("img").attr('src', node.img);
+ scope.dragElement = this.nodeHtmlSubstitute.clone().show();
+
+ return scope.dragElement;
+ };
+ }
+
+ private _searchComponents = (searchText: string, categories: any): void => {
+ let displaySortedCategories = angular.copy(categories);
+ if (searchText != '') {
+ angular.forEach(categories, function (category: any, categoryKey) {
+
+ angular.forEach(category, function (subcategory: Array<Models.DisplayComponent>, subcategoryKey) {
+ let filteredResources = [];
+ angular.forEach(subcategory, function (component: Models.DisplayComponent) {
+
+ let resourceFilterTerm: string = component.searchFilterTerms;
+ if (resourceFilterTerm.indexOf(searchText.toLowerCase()) >= 0) {
+ filteredResources.push(component);
+ }
+ });
+ if (filteredResources.length > 0) {
+ displaySortedCategories[categoryKey][subcategoryKey] = filteredResources;
+ }
+ else {
+ delete displaySortedCategories[categoryKey][subcategoryKey];
+ }
+ });
+ if (!(Object.keys(displaySortedCategories[categoryKey]).length > 0)) {
+ delete displaySortedCategories[categoryKey];
+ }
+
+ });
+ }
+ return displaySortedCategories;
+ };
+
+ public static factory = ($log,
+ LeftPaletteLoaderService,
+ sdcConfig,
+ ComponentFactory,
+ ComponentInstanceFactory,
+ NodesFactory,
+ CompositionGraphGeneralUtils,
+ EventListenerService,
+ sdcMenu) => {
+ return new Palette($log,
+ LeftPaletteLoaderService,
+ sdcConfig,
+ ComponentFactory,
+ ComponentInstanceFactory,
+ NodesFactory,
+ CompositionGraphGeneralUtils,
+ EventListenerService,
+ sdcMenu);
+ };
+ }
+
+ Palette.factory.$inject = [
+ '$log',
+ 'LeftPaletteLoaderService',
+ 'sdcConfig',
+ 'ComponentFactory',
+ 'ComponentInstanceFactory',
+ 'NodesFactory',
+ 'CompositionGraphGeneralUtils',
+ 'EventListenerService',
+ 'sdcMenu'
+ ];
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.html b/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.html
new file mode 100644
index 0000000000..a8dd827927
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.html
@@ -0,0 +1,59 @@
+<div class="w-sdc-designer-leftbar">
+ <div class="w-sdc-designer-leftbar-title">Elements <span class="w-sdc-designer-leftbar-title-count">{{model.numberOfElements}}</span>
+ </div>
+
+ <div class="w-sdc-designer-leftbar-search">
+ <input type="text" class="w-sdc-designer-leftbar-search-input" placeholder="Search..."
+ data-ng-model="searchText" data-ng-change="searchComponents(searchText)"
+ ng-model-options="{ debounce: 500 }" data-tests-id="searchAsset"/>
+ <span class="w-sdc-search-icon leftbar" data-ng-class="{'cancel':searchText, 'magnification':!searchText}"
+ data-ng-click="searchText=''; searchComponents('',categories)"></span>
+ </div>
+ <div class="i-sdc-designer-leftbar-section"
+ data-ng-repeat="(entityCategory, objCategory) in displaySortedCategories track by $index"
+ data-ng-class="{'expanded': expandedSection.indexOf(entityCategory) !== -1}">
+ <div class="i-sdc-designer-leftbar-section-title pointer" data-ng-click="sectionClick(entityCategory)"
+ data-tests-id="leftbar-section-title-{{entityCategory}}">
+ {{entityCategory}}
+ <div class="i-sdc-designer-leftbar-section-title-icon"></div>
+ </div>
+ <div class="i-sdc-designer-leftbar-section-content"
+ data-ng-repeat="(subCategory, components) in objCategory track by $index">
+ <div class="i-sdc-designer-leftbar-section-content-subcat i-sdc-designer-leftbar-section-content-item">
+ {{subCategory}}
+ </div>
+ <div class="i-sdc-designer-leftbar-section-content-item"
+ data-ng-class="{'default-pointer': isViewOnly}"
+ data-ng-mouseover="!isViewOnly && onMouseOver(component)"
+ data-ng-mouseleave="!isViewOnly && onMouseOut()"
+ data-drag="{{!isViewOnly}}"
+ data-jqyoui-options="{revert: 'invalid', helper:setElementTemplate, appendTo:'body', cursorAt: {left:38, top: 38}, cursor:'move'}"
+ jqyoui-draggable="{index:{{$index}},animate:true,onStart:'dragStartCallback(component)',onStop:'dragStopCallback()', onDrag:'onDragCallback()'}"
+ data-ng-repeat="component in components | orderBy: 'displayName' track by $index"
+ data-tests-id={{component.displayName}}>
+ <div class="i-sdc-designer-leftbar-section-content-item-icon-ph">
+ <div class="medium {{component.iconClass}}"
+ data-tests-id="leftbar-section-content-item-{{component.displayName}}">
+ <div class="{{component.certifiedIconClass}}" uib-tooltip="Not certified"
+ tooltip-class="uib-custom-tooltip" tooltip-placement="bottom" tooltip-popup-delay="700">
+ </div>
+ </div>
+ </div>
+ <div class="i-sdc-designer-leftbar-section-content-item-info">
+ <span class="i-sdc-designer-leftbar-section-content-item-info-title"
+ uib-tooltip="{{component.displayName}}" tooltip-class="uib-custom-tooltip"
+ tooltip-placement="bottom" tooltip-popup-delay="700">
+ {{component.displayName}}</span>
+ <div class="i-sdc-designer-leftbar-section-content-item-info-text">
+ V.{{component.version}}
+ </div>
+ <div class="i-sdc-designer-leftbar-section-content-item-info-text"> Type:
+ {{component.componentSubType}}
+ <a data-ng-click="openViewerModal(component)"
+ class="i-sdc-designer-leftbar-section-content-item-info-text-link hand">More</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.less b/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.less
new file mode 100644
index 0000000000..85657a43a5
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.less
@@ -0,0 +1,92 @@
+.drag-icon-border{
+ border: 7px solid red;
+ border-radius: 500px;
+ -webkit-border-radius: 500px;
+ -moz-border-radius: 500px;
+ width: 53px;
+ height: 53px;
+}
+
+.drag-icon-circle{
+ width: 60px;
+ height: 60px;
+ -webkit-border-radius: 50%;
+ -moz-border-radius: 50%;
+ border-radius: 50%;
+ position: relative;
+
+}
+
+
+@green-shadow: rgba(29, 154, 149, 0.3);
+@red-shadow: rgba(218, 31, 61, 0.3);
+.drag-icon-circle .sprite-resource-icons {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+}
+
+.drag-icon-circle.red {
+ background: @red-shadow;
+}
+
+.drag-icon-circle.green {
+ background: @green-shadow;
+}
+
+
+.node-substitute {
+ display: none;
+ position: absolute;
+ z-index: 9999;
+ height: 80px;
+ width: 80px;
+ border-radius: 50%;
+ text-align: center;
+
+ span {
+ display: inline-block;
+ vertical-align: middle;
+ height: 100%;
+ }
+
+ img {
+ height: 40px;
+ width: 40px;
+ box-shadow: 0 0 0 10px @green-shadow;
+ border-radius: 50%;
+
+ -webkit-user-drag: none;
+ -moz-user-drag: none;
+ user-drag: none;
+ }
+ &.red img {
+ box-shadow: 0 0 0 10px @red-shadow;
+ }
+ &.bounce img {
+ -moz-animation:bounceOut 0.3s linear;
+ -webkit-animation:bounceOut 0.3s linear;
+ animation:bounceOut 0.3s linear;
+ }
+}
+
+@keyframes bounceOut {
+ 0%{ box-shadow: 0 0 0 10px @green-shadow; width: 40px; height: 40px; }
+ 60%{ box-shadow: 0 0 0 0px @green-shadow; width: 60px; height: 60px; }
+ 85%{ box-shadow: 0 0 0 0px @green-shadow; width: 75px; height: 75px; }
+ 100%{ box-shadow: 0 0 0 0px @green-shadow; width: 60px; height: 60px; }
+}
+
+@-moz-keyframes bounceOut {
+ 0%{ box-shadow: 0 0 0 10px @green-shadow; width: 40px; height: 40px; }
+ 60%{ box-shadow: 0 0 0 0px @green-shadow; width: 60px; height: 60px; }
+ 85%{ box-shadow: 0 0 0 0px @green-shadow; width: 75px; height: 75px; }
+ 100%{ box-shadow: 0 0 0 0px @green-shadow; width: 60px; height: 60px; }
+}
+
+@-webkit-keyframes bounceOut {
+ 0%{ box-shadow: 0 0 0 10px @green-shadow; width: 40px; height: 40px; }
+ 60%{ box-shadow: 0 0 0 0px @green-shadow; width: 60px; height: 60px; }
+ 85%{ box-shadow: 0 0 0 0px @green-shadow; width: 75px; height: 75px; }
+ 100%{ box-shadow: 0 0 0 0px @green-shadow; width: 60px; height: 60px; }
+}
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.html b/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.html
new file mode 100644
index 0000000000..a0a9e4af27
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.html
@@ -0,0 +1,63 @@
+<div class="link-menu-open" data-tests-id="link-menu-open" data-ng-show="isLinkMenuOpen" ng-style="{left: relationMenuDirectiveObj.menuPosition.x, top: relationMenuDirectiveObj.menuPosition.y}" clicked-outside="{onClickedOutside: 'hideRelationMatch()', clickedOutsideEnable: 'isLinkMenuOpen'}" >
+ <h4 sdc-smart-tooltip>{{relationMenuDirectiveObj.leftSideLink.componentInstance.name | resourceName}}</h4>
+ <h4 sdc-smart-tooltip>{{relationMenuDirectiveObj.rightSideLink.componentInstance.name | resourceName}}</h4>
+
+ <p>Select one of the options below to connect</p>
+
+ <perfect-scrollbar scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+ <div class="inner-title" data-ng-show="hasMatchesToShow(relationMenuDirectiveObj.leftSideLink.requirements, relationMenuDirectiveObj.rightSideLink.selectedMatch)">Requirements</div>
+ <div class="link-item" data-tests-id="link-item-requirements" data-ng-repeat="(req ,matchArr) in relationMenuDirectiveObj.leftSideLink.requirements"
+ data-ng-click="relationMenuDirectiveObj.leftSideLink.selectMatchArr(matchArr); updateSelectionText()"
+ data-ng-show="showMatch(relationMenuDirectiveObj.rightSideLink.selectedMatch, matchArr)"
+ data-ng-class="{ 'selected': relationMenuDirectiveObj.leftSideLink.selectedMatch === matchArr}">
+ <div sdc-smart-tooltip>{{matchArr[0].requirement.getFullTitle()}}</div>
+ </div>
+
+ <div class="inner-title" data-ng-show="hasMatchesToShow(relationMenuDirectiveObj.leftSideLink.capabilities, relationMenuDirectiveObj.rightSideLink.selectedMatch)">Capabilities</div>
+ <div class="link-item" data-tests-id="link-item-capabilities" data-ng-repeat="(cap, matchArr) in relationMenuDirectiveObj.leftSideLink.capabilities"
+ data-ng-click="relationMenuDirectiveObj.leftSideLink.selectMatchArr(matchArr); updateSelectionText()"
+ data-ng-show="showMatch(relationMenuDirectiveObj.rightSideLink.selectedMatch, matchArr)"
+ data-ng-class="{ 'selected': relationMenuDirectiveObj.leftSideLink.selectedMatch === matchArr}">
+ <div sdc-smart-tooltip>{{matchArr[0].capability.getFullTitle()}}</div>
+ </div>
+ </perfect-scrollbar>
+
+ <perfect-scrollbar scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+ <div class="inner-title" data-ng-show="hasMatchesToShow(relationMenuDirectiveObj.rightSideLink.requirements, relationMenuDirectiveObj.leftSideLink.selectedMatch)">Requirements</div>
+ <div class="link-item" data-tests-id="link-item-requirements" data-ng-repeat="(req, matchArr) in relationMenuDirectiveObj.rightSideLink.requirements"
+ data-ng-click="relationMenuDirectiveObj.rightSideLink.selectMatchArr(matchArr); updateSelectionText()"
+ data-ng-show="showMatch(relationMenuDirectiveObj.leftSideLink.selectedMatch, matchArr)"
+ data-ng-class="{ 'selected': relationMenuDirectiveObj.rightSideLink.selectedMatch === matchArr}">
+ <div sdc-smart-tooltip>{{matchArr[0].secondRequirement ? matchArr[0].secondRequirement.getFullTitle() : matchArr[0].requirement.getFullTitle()}}</div>
+ </div>
+
+ <div class="inner-title" data-ng-show="hasMatchesToShow(relationMenuDirectiveObj.rightSideLink.capabilities, relationMenuDirectiveObj.leftSideLink.selectedMatch)">Capabilities</div>
+ <div class="link-item" data-tests-id="link-item-capabilities" data-ng-repeat="(cap, matchArr) in relationMenuDirectiveObj.rightSideLink.capabilities"
+ data-ng-click="relationMenuDirectiveObj.rightSideLink.selectMatchArr(matchArr); updateSelectionText()"
+ data-ng-show="showMatch(relationMenuDirectiveObj.leftSideLink.selectedMatch, matchArr)"
+ data-ng-class="{ 'selected': relationMenuDirectiveObj.rightSideLink.selectedMatch === matchArr}">
+ <div sdc-smart-tooltip>{{matchArr[0].capability.getFullTitle()}}</div>
+ </div>
+ </perfect-scrollbar>
+
+ <div class="vl-type" data-ng-class="{'disabled': !relationMenuDirectiveObj.leftSideLink.selectedMatch[0].secondRequirement || !relationMenuDirectiveObj.rightSideLink.selectedMatch[0].secondRequirement}">
+ <sdc-radio-button sdc-model="relationMenuDirectiveObj.vlType" value="ptp"
+ disabled="!relationMenuDirectiveObj.leftSideLink.selectedMatch[0].secondRequirement || !relationMenuDirectiveObj.rightSideLink.selectedMatch[0].secondRequirement || !relationMenuDirectiveObj.p2pVL"
+ text="Point to point" elem-id="radioPTP" elem-name="vlType"></sdc-radio-button>
+
+ <sdc-radio-button sdc-model="relationMenuDirectiveObj.vlType" value="mptmp"
+ disabled="!relationMenuDirectiveObj.leftSideLink.selectedMatch[0].secondRequirement || !relationMenuDirectiveObj.rightSideLink.selectedMatch[0].secondRequirement || !relationMenuDirectiveObj.mp2mpVL"
+ text="Multi point" elem-id="radioMPTMP" elem-name="vlType"></sdc-radio-button>
+
+ <span class="sprite-new info-icon" tooltips tooltip-content="You are required to choose the type of the Virtual Link."></span>
+ </div>
+
+ <div class="result" sdc-smart-tooltip>&#8203;{{relationMenuDirectiveObj.selectionText}}
+
+ </div>
+
+ <button class="tlv-btn grey" data-tests-id="link-menu-button-cancel" data-ng-click="hideRelationMatch()">Cancel</button>
+ <button class="tlv-btn blue" data-tests-id="link-menu-button-connect" data-ng-disabled="!relationMenuDirectiveObj.leftSideLink.selectedMatch || !relationMenuDirectiveObj.rightSideLink.selectedMatch ||
+ (relationMenuDirectiveObj.leftSideLink.selectedMatch[0].secondRequirement && !relationMenuDirectiveObj.vlType)"
+ data-ng-click="saveRelation()">Connect</button>
+</div>
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.less b/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.less
new file mode 100644
index 0000000000..dea814dbec
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.less
@@ -0,0 +1,118 @@
+.link-menu-open {
+ display: block !important;
+ color: @main_color_m;
+ font-size: 14px;
+ position: absolute;
+ z-index: 99999;
+ border-radius: 2px;
+ background-color: #ffffff;
+ box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.5);
+ width: 460px;
+ height: 418px;
+
+ h4 {
+ width: 50%;
+ float: left;
+ background-color: @tlv_color_u;
+ font-size: 14px;
+ font-weight: bold;
+ line-height: 36px;
+ margin: 0;
+ padding: 0 15px;
+
+ & + h4 {
+ border-left: #d8d8d8 1px solid;
+ }
+ }
+ p {
+ clear: both;
+ text-indent: 15px;
+ border-bottom: #d8d8d8 1px solid;
+ line-height: 34px;
+ margin: 0;
+ color: @func_color_s;
+ }
+
+ .scrollbar-container {
+ height: 232px;
+ width: 50%;
+ float: left;
+ margin-bottom: 5px;
+ .perfect-scrollbar;
+
+ & + .scrollbar-container {
+ border-left: #d8d8d8 1px solid;
+ }
+
+ .inner-title {
+ width: 189px;
+ margin: 5px auto 3px auto;
+ //text-indent: 10px;
+ color: @func_color_s;
+ text-transform: uppercase;
+ font-weight: bold;
+
+ //&:not(:first-child) {
+ // margin-top: 10px;
+ //}
+ }
+
+ .link-item {
+ padding: 0 10px;
+ line-height: 23px;
+ height: 23px;
+ text-indent: 5px;
+ .hand;
+
+ &.selected {
+ background-color: @tlv_color_v;
+ }
+ }
+ }
+
+ .vl-type {
+ height: 33px;
+ border-top: #d8d8d8 solid 1px;
+ clear: both;
+ padding: 0 10px;
+ line-height: 32px;
+ color: @main_color_m;
+
+ &.disabled {
+ background-color: #f2f2f2;
+ color: @color_m;
+ }
+ .info-icon {
+ float:right;
+ margin-top: 9px;
+ }
+ .tlv-radio {
+ margin-right: 10px;
+ }
+ }
+
+ .result {
+ background-color: @main_color_m;
+ line-height: 29px;
+ color: #ffffff;
+ padding: 0 15px;
+ }
+
+ button {
+ float: right;
+ margin-top: 9px;
+ margin-right: 10px;
+ }
+}
+.link-menu-item {
+ cursor: pointer;
+ line-height: 24px;
+ padding: 0 10px;
+ &:hover {
+ color: @color_a;
+ }
+}
+.link-menu::before {
+ right: inherit !important;
+ left: 50px;
+} \ No newline at end of file
diff --git a/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.ts b/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.ts
new file mode 100644
index 0000000000..22a2d078b7
--- /dev/null
+++ b/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.ts
@@ -0,0 +1,113 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. 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.
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+/// <reference path="../../../references"/>
+module Sdc.Directives {
+ 'use strict';
+
+
+ export interface IRelationMenuScope extends ng.IScope {
+ relationMenuDirectiveObj:Models.RelationMenuDirectiveObj;
+ createRelation:Function;
+ isLinkMenuOpen:boolean;
+ hideRelationMatch:Function;
+ cancel:Function;
+
+ saveRelation();
+ showMatch(arr1:Array<Models.MatchBase>, arr2:Array<Models.MatchBase>):boolean;
+ hasMatchesToShow(matchesObj:Models.MatchBase, selectedMatch:Array<Models.MatchBase>);
+ updateSelectionText():void;
+
+ }
+
+
+ export class RelationMenuDirective implements ng.IDirective {
+
+ constructor(private $templateCache:ng.ITemplateCacheService,
+ private $filter:ng.IFilterService
+ ) {
+ }
+
+ scope = {
+ relationMenuDirectiveObj: '=',
+ isLinkMenuOpen: '=',
+ createRelation: '&',
+ cancel:'&'
+ };
+
+ restrict = 'E';
+ replace = true;
+ template = ():string => {
+ return this.$templateCache.get('/app/scripts/directives/graphs-v2/relation-menu/relation-menu.html');
+ };
+
+ link = (scope:IRelationMenuScope, element:JQuery, $attr:ng.IAttributes) => {
+
+ scope.saveRelation = ():void=> {
+ let chosenMatches:Array<any> = _.intersection(scope.relationMenuDirectiveObj.rightSideLink.selectedMatch, scope.relationMenuDirectiveObj.leftSideLink.selectedMatch);
+ let chosenMatch:Models.MatchBase = chosenMatches[0];
+ let chosenVL:Models.Components.Component;
+ if ("mptmp" === scope.relationMenuDirectiveObj.vlType) {
+ chosenVL = scope.relationMenuDirectiveObj.mp2mpVL;
+ } else {
+ chosenVL = scope.relationMenuDirectiveObj.p2pVL;
+ }
+ scope.createRelation()(chosenMatch,chosenVL);
+ };
+
+
+ scope.hideRelationMatch = () => {
+ scope.isLinkMenuOpen = false;
+ scope.cancel();
+ };
+
+ //to show options in link menu
+ scope.showMatch = (arr1:Array<Models.MatchBase>, arr2:Array<Models.MatchBase>):boolean => {
+ return !arr1 || !arr2 || _.intersection(arr1, arr2).length > 0;
+ };
+
+ //to show requirements/capabilities title
+ scope.hasMatchesToShow = (matchesObj:Models.MatchBase, selectedMatch:Array<Models.MatchBase>):boolean => {
+ let result:boolean = false;
+ _.forEach(matchesObj, (matchesArr:Array<Models.MatchBase>) => {
+ if (!result) {
+ result = scope.showMatch(matchesArr, selectedMatch);
+ }
+ });
+ return result;
+ };
+
+
+ scope.updateSelectionText = ():void => {
+ let left:string = scope.relationMenuDirectiveObj.leftSideLink.selectedMatch ? this.$filter('resourceName')(scope.relationMenuDirectiveObj.leftSideLink.selectedMatch[0].getDisplayText('left')) : '';
+ let both:string = scope.relationMenuDirectiveObj.leftSideLink.selectedMatch && scope.relationMenuDirectiveObj.rightSideLink.selectedMatch ? ' - ' +
+ this.$filter('resourceName')(scope.relationMenuDirectiveObj.leftSideLink.selectedMatch[0].requirement.relationship) + ' - ' : '';
+ let right:string = scope.relationMenuDirectiveObj.rightSideLink.selectedMatch ? this.$filter('resourceName')(scope.relationMenuDirectiveObj.rightSideLink.selectedMatch[0].getDisplayText('right')) : '';
+ scope.relationMenuDirectiveObj.selectionText = left + both + right;
+ };
+
+
+ }
+ public static factory = ($templateCache:ng.ITemplateCacheService , $filter:ng.IFilterService)=> {
+ return new RelationMenuDirective($templateCache, $filter);
+ };
+ }
+
+ RelationMenuDirective.factory.$inject = ['$templateCache', '$filter'];
+}