diff options
Diffstat (limited to 'catalog-ui/app/scripts/directives/graphs-v2')
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>​{{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']; +} |