diff options
Diffstat (limited to 'catalog-ui/src/app/directives/graphs-v2/composition-graph')
8 files changed, 1779 insertions, 0 deletions
diff --git a/catalog-ui/src/app/directives/graphs-v2/composition-graph/composition-graph.directive.ts b/catalog-ui/src/app/directives/graphs-v2/composition-graph/composition-graph.directive.ts new file mode 100644 index 0000000000..db03aa53fb --- /dev/null +++ b/catalog-ui/src/app/directives/graphs-v2/composition-graph/composition-graph.directive.ts @@ -0,0 +1,539 @@ +import { + MatchBase, + LinkMenu, + ComponentInstance, + LeftPaletteComponent, + Component, + RelationMenuDirectiveObj, + CompositionCiNodeBase, + CompositionCiNodeVl, + NodesFactory/*, + AssetPopoverObj*/ +} from "app/models"; +import {ComponentInstanceFactory, ComponentFactory, GRAPH_EVENTS, GraphColors} from "app/utils"; +import {EventListenerService, LoaderService} from "app/services"; +import {CompositionGraphLinkUtils} from "./utils/composition-graph-links-utils"; +import {CompositionGraphGeneralUtils} from "./utils/composition-graph-general-utils"; +import {CompositionGraphNodesUtils} from "./utils/composition-graph-nodes-utils"; +import {CommonGraphUtils} from "../common/common-graph-utils"; +import {MatchCapabilitiesRequirementsUtils} from "./utils/match-capability-requierment-utils"; +import {CompositionGraphPaletteUtils} from "./utils/composition-graph-palette-utils"; +import {ComponentInstanceNodesStyle} from "../common/style/component-instances-nodes-style"; +import {CytoscapeEdgeEditation} from 'third-party/cytoscape.js-edge-editation/CytoscapeEdgeEditation.js'; +import {ComponentServiceNg2} from "../../../ng2/services/component-services/component.service"; +import {ComponentGenericResponse} from "../../../ng2/services/responses/component-generic-response"; + +interface ICompositionGraphScope extends ng.IScope { + + component:Component; + isLoading: boolean; + isViewOnly:boolean; + // Link menu - create link menu + relationMenuDirectiveObj:RelationMenuDirectiveObj; + isLinkMenuOpen:boolean; + createLinkFromMenu:(chosenMatch:MatchBase, vl:Component)=>void; + + //modify link menu - for now only delete menu + relationMenuTimeout:ng.IPromise<any>; + linkMenuObject: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(); + /*//asset popover menu + assetPopoverObj:AssetPopoverObj; + assetPopoverOpen:boolean; + hideAssetPopover():void; + deleteNode(nodeId:string):void;*/ +} + +export class CompositionGraph implements ng.IDirective { + private _cy:Cy.Instance; + private _currentlyCLickedNodePosition:Cy.Position; + // private $document:JQuery = $(document); + private dragElement:JQuery; + private dragComponent:ComponentInstance; + + constructor(private $q:ng.IQService, + private $log:ng.ILogService, + private $timeout:ng.ITimeoutService, + private NodesFactory:NodesFactory, + private CompositionGraphLinkUtils:CompositionGraphLinkUtils, + private GeneralGraphUtils:CompositionGraphGeneralUtils, + private ComponentInstanceFactory:ComponentInstanceFactory, + private NodesGraphUtils:CompositionGraphNodesUtils, + private eventListenerService:EventListenerService, + private ComponentFactory:ComponentFactory, + private LoaderService:LoaderService, + private commonGraphUtils:CommonGraphUtils, + private matchCapabilitiesRequirementsUtils:MatchCapabilitiesRequirementsUtils, + private CompositionGraphPaletteUtils:CompositionGraphPaletteUtils, + private ComponentServiceNg2: ComponentServiceNg2) { + + } + + restrict = 'E'; + template = require('./composition-graph.html'); + scope = { + component: '=', + isViewOnly: '=' + }; + + link = (scope:ICompositionGraphScope, el:JQuery) => { + + this.loadGraph(scope, el); + + if(scope.component.componentInstances && scope.component.componentInstancesRelations) { + this.loadGraphData(scope); + } else { + //when we don't have the data we register to on graph load event + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_COMPOSITION_GRAPH_DATA_LOADED, () => { + this.loadGraphData(scope); + }); + } + scope.$on('$destroy', () => { + this._cy.destroy(); + _.forEach(GRAPH_EVENTS, (event) => { + this.eventListenerService.unRegisterObserver(event); + }); + }); + + }; + + private loadGraphData = (scope:ICompositionGraphScope) => { + this.initGraphNodes(scope.component.componentInstances, scope.isViewOnly); + this.commonGraphUtils.initGraphLinks(this._cy, scope.component.componentInstancesRelations); + this.commonGraphUtils.initUcpeChildren(this._cy); + } + + private loadGraph = (scope:ICompositionGraphScope, el:JQuery) => { + + let graphEl = el.find('.sdc-composition-graph-wrapper'); + this.initGraph(graphEl, scope.isViewOnly); + 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: ComponentInstanceNodesStyle.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, (leftPaletteComponent:LeftPaletteComponent) => { + this.$log.info(`composition-graph::registerEventServiceEvents:: palette hover on component: ${leftPaletteComponent.uniqueId}`); + + let nodesData = this.NodesGraphUtils.getAllNodesData(this._cy.nodes()); + let nodesLinks = this.GeneralGraphUtils.getAllCompositionCiLinks(this._cy); + + if (this.GeneralGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(leftPaletteComponent.uniqueId)) { + let cacheComponent = this.GeneralGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(leftPaletteComponent.uniqueId); + let filteredNodesData = this.matchCapabilitiesRequirementsUtils.findByMatchingCapabilitiesToRequirements(cacheComponent, nodesData, nodesLinks); + + this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, this._cy); + this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, this._cy); + + return; + } + + //----------------------- ORIT TO FIX------------------------// + + this.ComponentServiceNg2.getCapabilitiesAndRequirements(leftPaletteComponent.componentType, leftPaletteComponent.uniqueId).subscribe((response: ComponentGenericResponse) => { + + let component = this.ComponentFactory.createEmptyComponent(leftPaletteComponent.componentType); + component.uniqueId = component.uniqueId; + component.capabilities = response.capabilities; + component.requirements = response.requirements; + this.GeneralGraphUtils.componentRequirementsAndCapabilitiesCaching.setValue(leftPaletteComponent.uniqueId, component); + let filteredNodesData = this.matchCapabilitiesRequirementsUtils.findByMatchingCapabilitiesToRequirements(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.CompositionGraphPaletteUtils.onComponentDrag(this._cy, event, this.dragElement, this.dragComponent); + + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_NAME_CHANGED, (component:ComponentInstance) => { + + let selectedNode = this._cy.getElementById(component.uniqueId); + selectedNode.data().componentInstance.name = component.name; + selectedNode.data('name', component.name); //used for tooltip + selectedNode.data('displayName', selectedNode.data().getDisplayName()); //abbreviated + + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, (componentInstance: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 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:Component) => { + scope.component = component; + this.loadGraphData(scope); + }); + + + scope.createLinkFromMenu = (chosenMatch:MatchBase):void => { + scope.isLinkMenuOpen = false; + this.CompositionGraphLinkUtils.createLinkFromMenu(this._cy, chosenMatch, 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); + } + }; + + /* + scope.hideAssetPopover = ():void => { + + this.commonGraphUtils.safeApply(scope, () => { + scope.assetPopoverOpen = false; + scope.assetPopoverObj = null; + }); + }; + + scope.deleteNode = (nodeId:string):void => { + if (!scope.isViewOnly) { + this.NodesGraphUtils.confirmDeleteNode(nodeId, this._cy, scope.component); + //scope.hideAssetPopover(); + } + };*/ + } + + 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); + } + //scope.hideAssetPopover(); + }); + + 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': GraphColors.NODE_BACKGROUND_COLOR}); + } else { + event.cyTarget.style({'overlay-color': 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('mouseover', 'node', (event:Cy.EventObject) => { + if (!this._cy.scratch('_edge_editation_highlights')) { + this.commonGraphUtils.safeApply(scope, () => { + this.showNodePopoverMenu(scope, event.cyTarget[0]); + }); + } + }); + + this._cy.on('mouseout', 'node', (event:Cy.EventObject) => { + scope.hideAssetPopover(); + });*/ + this._cy.on('handlemouseover', (event, payload) => { + + if (payload.node.grabbed() /* || this._cy.scratch('_edge_editation_highlights') === true*/) { //no need to add opacity while we are dragging and hovering othe nodes- or if opacity was already calculated for these 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.scratch()._edge_editation_highlights = true; + scope.hideAssetPopover();*/ + }); + + this._cy.on('handlemouseout', () => { + if (this._cy.scratch('_edge_editation_highlights') === true) { + this._cy.removeScratch('_edge_editation_highlights'); + 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(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(GRAPH_EVENTS.ON_NODE_SELECTED, event.cyTarget.data().componentInstance); + //open node popover menu + //this.showNodePopoverMenu(scope, event.cyTarget[0]); + }); + } + + if (isUcpe) { + this._cy.nodes('.ucpe-cp').lock(); + event.cyTarget.style('opacity', 1); + } + + } + }); + + this._cy.on('boxselect', 'node', (event:Cy.EventObject) => { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_NODE_SELECTED, event.cyTarget.data().componentInstance); + }); + } + + /* + private showNodePopoverMenu = (scope:ICompositionGraphScope, node:Cy.CollectionNodes) => { + + scope.assetPopoverObj = this.NodesGraphUtils.createAssetPopover(this._cy, node, scope.isViewOnly); + scope.assetPopoverOpen = true; + + };*/ + private openModifyLinkMenu = (scope:ICompositionGraphScope, linkMenuObject:LinkMenu, timeOutInMilliseconds?:number) => { + + this.commonGraphUtils.safeApply(scope, () => { + scope.linkMenuObject = linkMenuObject; + }); + + scope.relationMenuTimeout = this.$timeout(() => { + scope.hideRelationMenu(); + }, timeOutInMilliseconds ? timeOutInMilliseconds : 6000); + }; + + private initGraphNodes(componentInstances:ComponentInstance[], isViewOnly:boolean) { + + if (!isViewOnly) { //Init nodes handle extension - enable dynamic links + setTimeout(()=> { + let handles = new CytoscapeEdgeEditation; + handles.init(this._cy, 18); + handles.registerHandle(ComponentInstanceNodesStyle.getBasicNodeHanlde()); + handles.registerHandle(ComponentInstanceNodesStyle.getBasicSmallNodeHandle()); + handles.registerHandle(ComponentInstanceNodesStyle.getUcpeCpNodeHandle()); + }, 0); + } + + _.each(componentInstances, (instance) => { + let compositionGraphNode: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.CompositionGraphPaletteUtils.addNodeFromPalette(this._cy, event, scope.component); + }; + + 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; + } + } + + public static factory = ($q, + $log, + $timeout, + NodesFactory, + LinksGraphUtils, + GeneralGraphUtils, + ComponentInstanceFactory, + NodesGraphUtils, + EventListenerService, + ComponentFactory, + LoaderService, + CommonGraphUtils, + MatchCapabilitiesRequirementsUtils, + CompositionGraphPaletteUtils, + ComponentServiceNg2) => { + return new CompositionGraph( + $q, + $log, + $timeout, + NodesFactory, + LinksGraphUtils, + GeneralGraphUtils, + ComponentInstanceFactory, + NodesGraphUtils, + EventListenerService, + ComponentFactory, + LoaderService, + CommonGraphUtils, + MatchCapabilitiesRequirementsUtils, + CompositionGraphPaletteUtils, + ComponentServiceNg2); + } +} + +CompositionGraph.factory.$inject = [ + '$q', + '$log', + '$timeout', + 'NodesFactory', + 'CompositionGraphLinkUtils', + 'CompositionGraphGeneralUtils', + 'ComponentInstanceFactory', + 'CompositionGraphNodesUtils', + 'EventListenerService', + 'ComponentFactory', + 'LoaderService', + 'CommonGraphUtils', + 'MatchCapabilitiesRequirementsUtils', + 'CompositionGraphPaletteUtils', + 'ComponentServiceNg2' +]; diff --git a/catalog-ui/src/app/directives/graphs-v2/composition-graph/composition-graph.html b/catalog-ui/src/app/directives/graphs-v2/composition-graph/composition-graph.html new file mode 100644 index 0000000000..1e69d3384a --- /dev/null +++ b/catalog-ui/src/app/directives/graphs-v2/composition-graph/composition-graph.html @@ -0,0 +1,23 @@ +<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> + Delete + </div> + +</div> + +<!--<asset-popover ng-if="assetPopoverOpen" asset-popover-obj="assetPopoverObj" delete-asset="deleteNode(assetPopoverObj.nodeId)"></asset-popover>--> diff --git a/catalog-ui/src/app/directives/graphs-v2/composition-graph/composition-graph.less b/catalog-ui/src/app/directives/graphs-v2/composition-graph/composition-graph.less new file mode 100644 index 0000000000..56c8b5529d --- /dev/null +++ b/catalog-ui/src/app/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); + } +} diff --git a/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/composition-graph-general-utils.ts b/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/composition-graph-general-utils.ts new file mode 100644 index 0000000000..1303e7a894 --- /dev/null +++ b/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/composition-graph-general-utils.ts @@ -0,0 +1,244 @@ +import {ComponentInstance, Component, MatchReqToCapability, MatchBase, CompositionCiLinkBase, CompositionCiNodeUcpeCp} from "app/models"; +import {QueueUtils, Dictionary, GraphUIObjects} from "app/utils"; +import {LoaderService} from "app/services"; +import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils"; +import {CommonGraphUtils} from "../../common/common-graph-utils"; + + +export class CompositionGraphGeneralUtils { + + public componentRequirementsAndCapabilitiesCaching = new Dictionary<string, Component>(); + protected static graphUtilsUpdateQueue:QueueUtils; + + constructor(private $q:ng.IQService, + private LoaderService:LoaderService, + private commonGraphUtils:CommonGraphUtils, + private matchCapabilitiesRequirementsUtils:MatchCapabilitiesRequirementsUtils) { + CompositionGraphGeneralUtils.graphUtilsUpdateQueue = new 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).height() < menuPosition.y + GraphUIObjects.LINK_MENU_HEIGHT + $(document.getElementsByClassName('sdc-composition-graph-wrapper')).offset().top) { // if position menu is overflow bottom + menuPosition.y = $(document.body).height() - GraphUIObjects.TOP_HEADER_HEIGHT - 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 {MatchReqToCapability} + */ + public canBeHostedOn(cy:Cy.Instance, fromUcpeInstance:ComponentInstance, toComponentInstance:ComponentInstance):MatchReqToCapability { + + let matches:Array<MatchBase> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromUcpeInstance, toComponentInstance, this.getAllCompositionCiLinks(cy)); + let hostedOnMatch:MatchBase = _.find(matches, (match:MatchReqToCapability) => { + return match.requirement.capability.toLowerCase() === 'tosca.capabilities.container'; + }); + + return <MatchReqToCapability>hostedOnMatch; + }; + + + /** + * Checks whether node can be dropped into UCPE + * @param cy + * @param nodeToInsert + * @param ucpeNode + * @returns {boolean} + */ + private isValidDropInsideUCPE(cy:Cy.Instance, nodeToInsert:ComponentInstance, ucpeNode:ComponentInstance):boolean { + + let hostedOnMatch: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: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 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 { + let 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<CompositionCiLinkBase> => { + return _.map(cy.edges("[isSdcElement]"), (edge:Cy.CollectionEdges) => { + return edge.data(); + }); + }; + + + /** + * Get Graph Utils server queue + * @returns {QueueUtils} + */ + public getGraphUtilsServerUpdateQueue():QueueUtils { + return CompositionGraphGeneralUtils.graphUtilsUpdateQueue; + } + ; + + /** + * + * @param blockAction - true/false if this is a block action + * @param instances + * @param component + */ + public pushMultipleUpdateComponentInstancesRequestToQueue = (blockAction:boolean, instances:Array<ComponentInstance>, component: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:Component, blockAction:boolean, updatedInstance: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']; diff --git a/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/composition-graph-links-utils.ts b/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/composition-graph-links-utils.ts new file mode 100644 index 0000000000..314c761edd --- /dev/null +++ b/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/composition-graph-links-utils.ts @@ -0,0 +1,271 @@ +/** + * Created by obarda on 6/28/2016. + */ +import {GraphUIObjects, ComponentInstanceFactory, ResourceType} from "app/utils"; +import {LeftPaletteLoaderService, LoaderService} from "app/services"; +import { + NodeUcpe, + CompositionCiNodeVf, + MatchReqToCapability, + MatchBase, + MatchReqToReq, + ComponentInstance, + CompositionCiNodeBase, + RelationshipModel, + RelationMenuDirectiveObj, + CapabilitiesGroup, + LinksFactory, + NodesFactory, + RequirementsGroup, + Component, + Relationship, + Capability, + LinkMenu, + Point, + CompositionCiLinkBase +} from "app/models"; +import {CommonGraphUtils} from "../../common/common-graph-utils"; +import {CompositionGraphGeneralUtils} from "./composition-graph-general-utils"; +import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils"; + +export class CompositionGraphLinkUtils { + + constructor(private linksFactory:LinksFactory, + private loaderService:LoaderService, + private generalGraphUtils:CompositionGraphGeneralUtils, + private commonGraphUtils:CommonGraphUtils, + private matchCapabilitiesRequirementsUtils:MatchCapabilitiesRequirementsUtils) { + } + + + /** + * 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: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:CompositionCiLinkBase, cy:Cy.Instance, component:Component):void => { + + this.loaderService.showLoader('composition-graph'); + + let onSuccess:(response:RelationshipModel) => void = (relation: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') + ); + }; + + private createSimpleLink = (match:MatchReqToCapability, cy:Cy.Instance, component:Component):void => { + let newRelation:RelationshipModel = match.matchToRelationModel(); + let linkObg:CompositionCiLinkBase = this.linksFactory.createGraphLink(cy, newRelation, newRelation.relationships[0]); + this.createLink(linkObg, cy, component); + }; + + public createLinkFromMenu = (cy:Cy.Instance, chosenMatch:MatchBase, component:Component):void => { + + if (chosenMatch) { + if (chosenMatch && chosenMatch instanceof 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<MatchBase>} + */ + public filterUcpeLinks(fromNode:CompositionCiNodeBase, toNode:CompositionCiNodeBase, matchesArray:Array<MatchBase>):any { + + let matchLink:Array<MatchBase>; + + if (fromNode.isUcpePart) { + matchLink = _.filter(matchesArray, (match:MatchBase) => { + return match.isOwner(fromNode.id); + }); + } + + if (toNode.isUcpePart) { + matchLink = _.filter(matchesArray, (match: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):RelationMenuDirectiveObj { + + if (!this.commonGraphUtils.nodeLocationsCompatible(cy, fromNode, toNode)) { + return null; + } + let linkModel:Array<CompositionCiLinkBase> = this.generalGraphUtils.getAllCompositionCiLinks(cy); + + let possibleRelations:Array<MatchBase> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromNode.data().componentInstance, + toNode.data().componentInstance, linkModel); + + //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 RelationMenuDirectiveObj(fromNode.data(), toNode.data(), 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: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:Component, cy:Cy.Instance, ucpeNode:NodeUcpe, vfNode:CompositionCiNodeVf):void => { + let hostedOnMatch:MatchReqToCapability = this.generalGraphUtils.canBeHostedOn(cy, ucpeNode.componentInstance, vfNode.componentInstance); + /* create relation */ + let newRelation = new RelationshipModel(); + newRelation.fromNode = ucpeNode.id; + newRelation.toNode = vfNode.id; + + let link: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 {Point} + */ + public calculateLinkMenuPosition(event, elementWidth, elementHeight):Point { + let point:Point = new 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 {LinkMenu} + */ + public getModifyLinkMenu(link:Cy.CollectionFirstEdge, event:Cy.EventObject):LinkMenu { + let point:Point = this.calculateLinkMenuPosition(event, GraphUIObjects.MENU_LINK_VL_WIDTH_OFFSET, GraphUIObjects.MENU_LINK_VL_HEIGHT_OFFSET); + let menu:LinkMenu = new LinkMenu(point, true, link); + return menu; + }; + +} + + +CompositionGraphLinkUtils.$inject = [ + 'LinksFactory', + 'LoaderService', + 'CompositionGraphGeneralUtils', + 'CommonGraphUtils', + 'MatchCapabilitiesRequirementsUtils' +]; diff --git a/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/composition-graph-nodes-utils.ts b/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/composition-graph-nodes-utils.ts new file mode 100644 index 0000000000..96afc8a4ea --- /dev/null +++ b/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/composition-graph-nodes-utils.ts @@ -0,0 +1,266 @@ +import {Component, NodesFactory, ComponentInstance, CompositionCiNodeVl,IAppMenu,AssetPopoverObj} from "app/models"; +import {EventListenerService, LoaderService} from "app/services"; +import {GRAPH_EVENTS,ModalsHandler,GraphUIObjects} from "app/utils"; +import {CompositionGraphGeneralUtils} from "./composition-graph-general-utils"; +import {CommonGraphUtils} from "../../common/common-graph-utils"; +/** + * Created by obarda on 11/9/2016. + */ +export class CompositionGraphNodesUtils { + constructor(private NodesFactory:NodesFactory, private $log:ng.ILogService, + private GeneralGraphUtils:CompositionGraphGeneralUtils, + private commonGraphUtils:CommonGraphUtils, + private eventListenerService:EventListenerService, + private loaderService:LoaderService /*, + private sdcMenu: IAppMenu, + private ModalsHandler: ModalsHandler*/) { + + } + + /** + * 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:Component, nodeToDelete:Cy.CollectionNodes):void { + + this.loaderService.showLoader('composition-graph'); + let onSuccess:(response:ComponentInstance) => void = (response: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(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 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') + ); + + }; + +/* + public confirmDeleteNode = (nodeId:string, cy:Cy.Instance, component:Component) => { + let node:Cy.CollectionNodes = cy.getElementById(nodeId); + let onOk = ():void => { + this.deleteNode(cy, component, node); + }; + + let componentInstance:ComponentInstance = node.data().componentInstance; + let state = "deleteInstance"; + let title:string = this.sdcMenu.alertMessages[state].title; + let message:string = this.sdcMenu.alertMessages[state].message.format([componentInstance.name]); + + this.ModalsHandler.openAlertModal(title, message).then(onOk); + };*/ + /** + * 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 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(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:Component, cy:Cy.Instance, node:Cy.CollectionNodes):Cy.CollectionNodes => { + if (node.data() instanceof CompositionCiNodeVl) { + return; + } + + let connectedVLsToDelete:Cy.CollectionNodes = cy.collection(); + _.forEach(node.neighborhood('node'), (connectedNode) => { + + //Find all neighboring nodes that are VLs + if (connectedNode.data() instanceof 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: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<ComponentInstance> = new Array<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<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(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(GRAPH_EVENTS.ON_INSERT_NODE_TO_UCPE, node, ucpeContainer, true); + } + } + /** + * Gets the position for the asset popover menu + * Then, check if right edge of menu would overlap horizontal screen edge (palette offset + canvas width - right panel) + * Then, check if bottom edge of menu would overlap the vertical end of the canvas. + * @param cy + * @param node + * @returns {Cy.Position} + + public createAssetPopover = (cy: Cy.Instance, node:Cy.CollectionFirstNode, isViewOnly:boolean):AssetPopoverObj => { + + let menuOffset:Cy.Position = { x: node.renderedWidth() / 2, y: -(node.renderedWidth() / 2) };// getNodePositionWithOffset returns central point of node. First add node.renderedWidth()/2 to get its to border. + let menuPosition:Cy.Position = this.commonGraphUtils.getNodePositionWithOffset(node, menuOffset); + let menuSide:string = 'right'; + + if(menuPosition.x + GraphUIObjects.COMPOSITION_NODE_MENU_WIDTH >= cy.width() + GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET - GraphUIObjects.COMPOSITION_RIGHT_PANEL_OFFSET){ + menuPosition.x -= menuOffset.x * 2 + GraphUIObjects.COMPOSITION_NODE_MENU_WIDTH; //menu position already includes offset to the right. Therefore, subtract double offset so we have same distance from node for menu on left + menuSide = 'left'; + } + + if(menuPosition.y + GraphUIObjects.COMPOSITION_NODE_MENU_HEIGHT >= cy.height()){ + menuPosition.y = menuPosition.y - GraphUIObjects.COMPOSITION_NODE_MENU_HEIGHT - menuOffset.y * 2; + } + + return new AssetPopoverObj(node.data().id, node.data().name, menuPosition, menuSide, isViewOnly); + }; + */ + + } + + + CompositionGraphNodesUtils.$inject = ['NodesFactory', '$log', 'CompositionGraphGeneralUtils', 'CommonGraphUtils', 'EventListenerService', 'LoaderService' /*, 'sdcMenu', 'ModalsHandler'*/] + diff --git a/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/composition-graph-palette-utils.ts b/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/composition-graph-palette-utils.ts new file mode 100644 index 0000000000..83bf747501 --- /dev/null +++ b/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/composition-graph-palette-utils.ts @@ -0,0 +1,163 @@ +import {EventListenerService, LoaderService} from "app/services"; +import {CapabilitiesGroup, NodesFactory, ComponentInstance, Component, CompositionCiNodeBase, RequirementsGroup} from "app/models"; +import {ComponentFactory, ComponentInstanceFactory, GRAPH_EVENTS, GraphUIObjects} from "app/utils"; +import {CompositionGraphGeneralUtils} from "./composition-graph-general-utils"; +import {CommonGraphUtils} from "../../common/common-graph-utils"; +import 'angular-dragdrop'; +import {LeftPaletteComponent} from "../../../../models/components/displayComponent"; + +export class CompositionGraphPaletteUtils { + + constructor(private ComponentFactory:ComponentFactory, + private $filter:ng.IFilterService, + private loaderService:LoaderService, + private generalGraphUtils:CompositionGraphGeneralUtils, + private componentInstanceFactory:ComponentInstanceFactory, + private nodesFactory:NodesFactory, + private commonGraphUtils:CommonGraphUtils, + private eventListenerService:EventListenerService) { + } + + /** + * Calculate the dragged element (html element) position on canvas + * @param cy + * @param event + * @param position + * @returns {Cy.BoundingBox} + * @private + */ + private _getNodeBBox(cy:Cy.Instance, event:IDragDropEvent, position?:Cy.Position) { + let bbox = <Cy.BoundingBox>{}; + if (!position) { + position = this.commonGraphUtils.getCytoscapeNodePosition(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; + } + + /** + * Create the component instance, update data from parent component in the left palette and notify on_insert_to_ucpe if component was dragg into ucpe + * @param cy + * @param fullComponent + * @param event + * @param component + */ + private _createComponentInstanceOnGraphFromPaletteComponent(cy:Cy.Instance, fullComponent:LeftPaletteComponent, event:IDragDropEvent, component:Component) { + + let componentInstanceToCreate:ComponentInstance = this.componentInstanceFactory.createComponentInstanceFromComponent(fullComponent); + let cytoscapePosition:Cy.Position = this.commonGraphUtils.getCytoscapeNodePosition(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:ComponentInstance):void => { + + this.loaderService.hideLoader('composition-graph'); + + createInstance.name = this.$filter('resourceName')(createInstance.name); + createInstance.requirements = new RequirementsGroup(createInstance.requirements); + createInstance.capabilities = new CapabilitiesGroup(createInstance.capabilities); + createInstance.componentVersion = fullComponent.version; + createInstance.icon = fullComponent.icon; + createInstance.setInstanceRC(); + + let newNode:CompositionCiNodeBase = this.nodesFactory.createNode(createInstance); + let cyNode:Cy.CollectionNodes = this.commonGraphUtils.addComponentInstanceNodeToGraph(cy, newNode); + + //check if node was dropped into a UCPE + let ucpe:Cy.CollectionElements = this.commonGraphUtils.isInUcpe(cy, cyNode.boundingbox()); + if (ucpe.length > 0) { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_INSERT_NODE_TO_UCPE, cyNode, ucpe, false); + } + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CREATE_COMPONENT_INSTANCE); + + }; + + this.loaderService.showLoader('composition-graph'); + + // Create the component instance on server + this.generalGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIAction(() => { + component.createComponentInstance(componentInstanceToCreate).then(onSuccessCreatingInstance, onFailedCreatingInstance); + }); + } + + /** + * Thid function applay red/green background when component dragged from palette + * @param cy + * @param event + * @param dragElement + * @param dragComponent + */ + public onComponentDrag(cy:Cy.Instance, event:IDragDropEvent, dragElement:JQuery, dragComponent:ComponentInstance) { + + if (event.clientX < GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET || event.clientY < GraphUIObjects.DIAGRAM_HEADER_OFFSET) { //hovering over palette. Dont bother computing validity of drop + dragElement.removeClass('red'); + return; + } + + let offsetPosition = { + x: event.clientX - GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET, + y: event.clientY - GraphUIObjects.DIAGRAM_HEADER_OFFSET + }; + let bbox = this._getNodeBBox(cy, event, offsetPosition); + + if (this.generalGraphUtils.isPaletteDropValid(cy, bbox, dragComponent)) { + dragElement.removeClass('red'); + } else { + dragElement.addClass('red'); + } + } + + /** + * This function is called when after dropping node on canvas + * Check if the capability & requirements fulfilled and if not get from server + * @param cy + * @param event + * @param component + */ + public addNodeFromPalette(cy:Cy.Instance, event:IDragDropEvent, component:Component) { + this.loaderService.showLoader('composition-graph'); + + let draggedComponent:LeftPaletteComponent = event.dataTransfer.component; + + if (this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(draggedComponent.uniqueId)) { + let fullComponent = this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(draggedComponent.uniqueId); + draggedComponent.capabilities = fullComponent.capabilities; + draggedComponent.requirements = fullComponent.requirements; + this._createComponentInstanceOnGraphFromPaletteComponent(cy, draggedComponent, event, component); + + } else { + + this.ComponentFactory.getComponentFromServer(draggedComponent.getComponentSubType(), draggedComponent.uniqueId) + .then((fullComponent:Component) => { + draggedComponent.capabilities = fullComponent.capabilities; + draggedComponent.requirements = fullComponent.requirements; + this._createComponentInstanceOnGraphFromPaletteComponent(cy, draggedComponent, event, component); + }); + } + } +} + + +CompositionGraphPaletteUtils.$inject = [ + 'ComponentFactory', + '$filter', + 'LoaderService', + 'CompositionGraphGeneralUtils', + 'ComponentInstanceFactory', + 'NodesFactory', + 'CommonGraphUtils', + 'EventListenerService' +]; diff --git a/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/match-capability-requierment-utils.ts b/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/match-capability-requierment-utils.ts new file mode 100644 index 0000000000..0e21f033be --- /dev/null +++ b/catalog-ui/src/app/directives/graphs-v2/composition-graph/utils/match-capability-requierment-utils.ts @@ -0,0 +1,259 @@ +import {Requirement, CompositionCiLinkBase, ComponentInstance, CapabilitiesGroup, RequirementsGroup, MatchReqToCapability, MatchBase, + MatchReqToReq,CompositionCiNodeBase, Component, Capability} from "app/models"; +/** + * Created by obarda on 1/1/2017. + */ + +export class MatchCapabilitiesRequirementsUtils { + + constructor() { + } + + public static linkable(requirement1:Requirement, requirement2:Requirement, vlCapability: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<CompositionCiLinkBase>):boolean { + return _.some(links, { + 'relation': { + 'fromNode': fromNodeId, + 'relationships': [{ + 'requirementOwnerId': requirement.ownerId, + 'requirement': requirement.name, + 'relationship': { + 'type': requirement.relationship + } + } + ] + } + }); + }; + + private static isMatch(requirement:Requirement, capability: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:RequirementsGroup, + requirements2:RequirementsGroup, + capabilities:CapabilitiesGroup, + links:Array<CompositionCiLinkBase>, + fromId:string, + toId:string, + vlCapability?:Capability):Array<MatchBase> { + let matches:Array<MatchBase> = new Array<MatchBase>(); + _.forEach(requirements1, (requirementValue:Array<Requirement>, key) => { + _.forEach(requirementValue, (requirement:Requirement) => { + if (requirement.name !== "dependency" && !MatchCapabilitiesRequirementsUtils.requirementFulfilled(fromId, requirement, links)) { + _.forEach(capabilities, (capabilityValue:Array<Capability>, key) => { + _.forEach(capabilityValue, (capability:Capability) => { + if (MatchCapabilitiesRequirementsUtils.isMatch(requirement, capability)) { + let match:MatchReqToCapability = new MatchReqToCapability(requirement, capability, true, fromId, toId); + matches.push(match); + } + }); + }); + if (vlCapability) { + _.forEach(requirements2, (requirement2Value:Array<Requirement>, key) => { + _.forEach(requirement2Value, (requirement2:Requirement) => { + if (!MatchCapabilitiesRequirementsUtils.requirementFulfilled(toId, requirement2, links) && MatchCapabilitiesRequirementsUtils.linkable(requirement, requirement2, vlCapability)) { + let match:MatchReqToReq = new MatchReqToReq(requirement, requirement2, true, fromId, toId); + matches.push(match); + } + }); + }); + } + } + }); + }); + return matches; + } + + private getToFromMatches(requirements:RequirementsGroup, capabilities:CapabilitiesGroup, links:Array<CompositionCiLinkBase>, fromId:string, toId:string):Array<MatchReqToCapability> { + let matches:Array<MatchReqToCapability> = []; + _.forEach(requirements, (requirementValue:Array<Requirement>, key) => { + _.forEach(requirementValue, (requirement:Requirement) => { + if (requirement.name !== "dependency" && !MatchCapabilitiesRequirementsUtils.requirementFulfilled(toId, requirement, links)) { + _.forEach(capabilities, (capabilityValue:Array<Capability>, key) => { + _.forEach(capabilityValue, (capability:Capability) => { + if (MatchCapabilitiesRequirementsUtils.isMatch(requirement, capability)) { + let match:MatchReqToCapability = new MatchReqToCapability(requirement, capability, false, toId, fromId); + matches.push(match); + } + }); + }); + } + }); + }); + return matches; + } + + public getMatchedRequirementsCapabilities(fromComponentInstance:ComponentInstance, + toComponentInstance:ComponentInstance, + links:Array<CompositionCiLinkBase>, + vl?:Component):Array<MatchBase> {//TODO allow for VL array + let linkCapability; + if (vl) { + let linkCapabilities:Array<Capability> = vl.capabilities.findValueByKey('linkable'); + if (linkCapabilities) { + linkCapability = linkCapabilities[0]; + } + } + let fromToMatches:Array<MatchBase> = this.getFromToMatches(fromComponentInstance.requirements, + toComponentInstance.requirements, + toComponentInstance.capabilities, + links, + fromComponentInstance.uniqueId, + toComponentInstance.uniqueId, + linkCapability); + let toFromMatches:Array<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:Component, + nodeDataArray:Array<CompositionCiNodeBase>, + links:Array<CompositionCiLinkBase>, + vl?: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: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<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 = []; |