Fix requirement external toggle when deselecting and reselecting node
[sdc.git] / catalog-ui / src / app / ng2 / pages / composition / graph / composition-graph.component.ts
1 /**
2  * Created by ob0695 on 4/24/2018.
3  */
4 import { AfterViewInit, Component, ElementRef, HostBinding, Input } from '@angular/core';
5 import { Select, Store } from '@ngxs/store';
6 import {
7     ButtonModel,
8     Component as TopologyTemplate,
9     ComponentInstance,
10     CompositionCiNodeBase,
11     ConnectRelationModel,
12     GroupInstance,
13     LeftPaletteComponent,
14     LinkMenu,
15     Match,
16     ModalModel,
17     NodesFactory,
18     Point,
19     PolicyInstance,
20     PropertyBEModel,
21     Relationship,
22     StepModel,
23     Zone,
24     ZoneInstance,
25     ZoneInstanceAssignmentType,
26     ZoneInstanceMode,
27     ZoneInstanceType,
28     Requirement
29 } from 'app/models';
30 import { ForwardingPath } from 'app/models/forwarding-path';
31 import { CompositionCiServicePathLink } from 'app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link';
32 import { UIZoneInstanceObject } from 'app/models/ui-models/ui-zone-instance-object';
33 import { CompositionService } from 'app/ng2/pages/composition/composition.service';
34 import { CommonGraphUtils } from 'app/ng2/pages/composition/graph/common/common-graph-utils';
35 import { ComponentInstanceNodesStyle } from 'app/ng2/pages/composition/graph/common/style/component-instances-nodes-style';
36 import { ConnectionPropertiesViewComponent } from 'app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component';
37 import { ConnectionWizardHeaderComponent } from 'app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component';
38 import { ConnectionWizardService } from 'app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service';
39 import { FromNodeStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component';
40 import { PropertiesStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component';
41 import { ToNodeStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component';
42 import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service';
43 import { ComponentInstanceServiceNg2 } from 'app/ng2/services/component-instance-services/component-instance.service';
44 import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service';
45 import { ModalService } from 'app/ng2/services/modal.service';
46 import { ComponentGenericResponse } from 'app/ng2/services/responses/component-generic-response';
47 import { ServiceGenericResponse } from 'app/ng2/services/responses/service-generic-response';
48 import { WorkspaceState } from 'app/ng2/store/states/workspace.state';
49 import { EventListenerService } from 'app/services';
50 import { ComponentInstanceFactory, EVENTS, SdcElementType } from 'app/utils';
51 import { ComponentType, GRAPH_EVENTS, GraphColors, DEPENDENCY_EVENTS , SUBSTITUTION_FILTER_EVENTS} from 'app/utils/constants';
52 import * as _ from 'lodash';
53 import { DndDropEvent } from 'ngx-drag-drop/ngx-drag-drop';
54 import { SdcUiServices } from 'onap-ui-angular';
55 import { NotificationSettings } from 'onap-ui-angular/dist/notifications/utilities/notification.config';
56 import { menuItem } from 'onap-ui-angular/dist/simple-popup-menu/menu-data.interface';
57 import { CytoscapeEdgeEditation } from '../../../../../third-party/cytoscape.js-edge-editation/CytoscapeEdgeEditation.js';
58 import { SelectedComponentType, SetSelectedComponentAction } from '../common/store/graph.actions';
59 import { GraphState } from '../common/store/graph.state';
60 import {
61     CompositionGraphGeneralUtils,
62     CompositionGraphNodesUtils,
63     CompositionGraphZoneUtils,
64     MatchCapabilitiesRequirementsUtils
65 } from './utils';
66 import { CompositionGraphLinkUtils } from './utils/composition-graph-links-utils';
67 import { CompositionGraphPaletteUtils } from './utils/composition-graph-palette-utils';
68 import { ServicePathGraphUtils } from './utils/composition-graph-service-path-utils';
69 import { RelationshipOperationsStepComponent } from "app/ng2/pages/composition/graph/connection-wizard/relationship-operations-step/relationship-operations-step.component";
70
71 declare const window: any;
72
73 @Component({
74     selector: 'composition-graph',
75     templateUrl: './composition-graph.component.html',
76     styleUrls: ['./composition-graph.component.less']
77 })
78
79 export class CompositionGraphComponent implements AfterViewInit {
80
81     @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean;
82     @Select(GraphState.withSidebar) withSidebar$: boolean;
83     @Input() topologyTemplate: TopologyTemplate;
84     @HostBinding('attr.data-tests-id') dataTestId: string;
85     @Input() testId: string;
86
87     // tslint:disable:variable-name
88     private _cy: Cy.Instance;
89     private zoneTagMode: string;
90     private activeZoneInstance: ZoneInstance;
91     private zones: Zone[];
92     private currentlyClickedNodePosition: Cy.Position;
93     private dragElement: JQuery;
94     private dragComponent: ComponentInstance;
95     private componentInstanceNames: string[];
96     private topologyTemplateId: string;
97     private topologyTemplateType: string;
98
99     constructor(private elRef: ElementRef,
100                 private nodesFactory: NodesFactory,
101                 private eventListenerService: EventListenerService,
102                 private compositionGraphZoneUtils: CompositionGraphZoneUtils,
103                 private generalGraphUtils: CompositionGraphGeneralUtils,
104                 private compositionGraphLinkUtils: CompositionGraphLinkUtils,
105                 private nodesGraphUtils: CompositionGraphNodesUtils,
106                 private connectionWizardService: ConnectionWizardService,
107                 private commonGraphUtils: CommonGraphUtils,
108                 private modalService: ModalService,
109                 private compositionGraphPaletteUtils: CompositionGraphPaletteUtils,
110                 private topologyTemplateService: TopologyTemplateService,
111                 private componentInstanceService: ComponentInstanceServiceNg2,
112                 private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils,
113                 private store: Store,
114                 private compositionService: CompositionService,
115                 private loaderService: SdcUiServices.LoaderService,
116                 private workspaceService: WorkspaceService,
117                 private notificationService: SdcUiServices.NotificationsService,
118                 private simplePopupMenuService: SdcUiServices.simplePopupMenuService,
119                 private servicePathGraphUtils: ServicePathGraphUtils) {
120     }
121
122     ngOnInit() {
123         this.dataTestId = this.testId;
124         this.topologyTemplateId = this.workspaceService.metadata.uniqueId;
125         this.topologyTemplateType = this.workspaceService.metadata.componentType;
126
127         this.store.dispatch(new SetSelectedComponentAction({
128             component: this.topologyTemplate,
129             type: SelectedComponentType.TOPOLOGY_TEMPLATE
130         }));
131         this.eventListenerService.registerObserverCallback(EVENTS.ON_CHECKOUT, () => {
132             this.loadGraphData();
133         });
134         this.loadCompositionData();
135     }
136
137     ngAfterViewInit() {
138         this.loadGraph();
139     }
140
141     ngOnDestroy() {
142         this._cy.destroy();
143         _.forEach(GRAPH_EVENTS, (event) => {
144             this.eventListenerService.unRegisterObserver(event);
145         });
146         this.eventListenerService.unRegisterObserver(EVENTS.ON_CHECKOUT);
147         this.eventListenerService.unRegisterObserver(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE);
148         this.eventListenerService.unRegisterObserver(SUBSTITUTION_FILTER_EVENTS.ON_SUBSTITUTION_FILTER_CHANGE);
149     }
150
151     public isViewOnly = (): boolean => {
152         return this.store.selectSnapshot((state) => state.workspace.isViewOnly);
153     }
154
155     public zoom = (zoomIn: boolean): void => {
156         const currentZoom: number = this._cy.zoom();
157         if (zoomIn) {
158             this.generalGraphUtils.zoomGraphTo(this._cy, currentZoom + .1);
159         } else {
160             this.generalGraphUtils.zoomGraphTo(this._cy, currentZoom - .1);
161         }
162     }
163
164     public zoomAllWithoutSidebar = () => {
165         setTimeout(() => { // wait for sidebar changes to take effect before zooming
166             this.generalGraphUtils.zoomAll(this._cy);
167         });
168     }
169
170     public getAutoCompleteValues = (searchTerm: string) => {
171         if (searchTerm.length > 1) { // US requirement: only display search results after 2nd letter typed.
172             const nodes: Cy.CollectionNodes = this.nodesGraphUtils.getMatchingNodesByName(this._cy, searchTerm);
173             this.componentInstanceNames = _.map(nodes, (node) => node.data('name'));
174         } else {
175             this.componentInstanceNames = [];
176         }
177     }
178
179     public highlightSearchMatches = (searchTerm: string) => {
180         this.nodesGraphUtils.highlightMatchingNodesByName(this._cy, searchTerm);
181         const matchingNodes: Cy.CollectionNodes = this.nodesGraphUtils.getMatchingNodesByName(this._cy, searchTerm);
182         this.generalGraphUtils.zoomAll(this._cy, matchingNodes);
183     }
184
185     public onDrop = (dndEvent: DndDropEvent) => {
186         this.compositionGraphPaletteUtils.addNodeFromPalette(this._cy, dndEvent);
187     }
188
189     public openServicePathMenu = ($event): void => {
190
191         const menuConfig: menuItem[] = [];
192         if (!this.isViewOnly()) {
193             menuConfig.push({
194                 text: 'Create Service Flow',
195                 action: () => this.servicePathGraphUtils.onCreateServicePath()
196             });
197         }
198         menuConfig.push({
199             text: 'Service Flows List',
200             type: '',
201             action: () => this.servicePathGraphUtils.onListServicePath()
202         });
203         const popup = this.simplePopupMenuService.openBaseMenu(menuConfig, {
204             x: $event.x,
205             y: $event.y
206         });
207
208     }
209
210     public deletePathsOnCy = () => {
211         this.servicePathGraphUtils.deletePathsFromGraph(this._cy);
212     }
213
214     public drawPathOnCy = (data: ForwardingPath) => {
215         this.servicePathGraphUtils.drawPath(this._cy, data);
216     }
217
218     public onTapEnd = (event: Cy.EventObject) => {
219         if (this.zoneTagMode) {
220             return;
221         }
222         if (event.cyTarget === this._cy) { // On Background clicked
223             if (this._cy.$('node:selected').length === 0) { // if the background click but not dragged
224                 if (this.activeZoneInstance) {
225                     this.unsetActiveZoneInstance();
226                     this.selectTopologyTemplate();
227                 } else {
228                     this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED);
229                     this.selectTopologyTemplate();
230                 }
231
232             }
233         } else if (event.cyTarget[0].isEdge()) { // and Edge clicked
234             this.compositionGraphLinkUtils.handleLinkClick(this._cy, event);
235             if (event.cyTarget[0].data().type === CompositionCiServicePathLink.LINK_TYPE) {
236                 return;
237             }
238             this.openModifyLinkMenu(this.compositionGraphLinkUtils.getModifyLinkMenu(event.cyTarget[0], event), event);
239         } else { // On Node clicked
240
241             this._cy.nodes(':grabbed').style({'overlay-opacity': 0});
242
243             const newPosition = event.cyTarget[0].position();
244             // node position changed (drop after drag event) - we need to update position
245             if (this.currentlyClickedNodePosition.x !== newPosition.x || this.currentlyClickedNodePosition.y !== newPosition.y) {
246                 const nodesMoved: Cy.CollectionNodes = this._cy.$(':grabbed');
247                 this.nodesGraphUtils.onNodesPositionChanged(this._cy, this.topologyTemplate, nodesMoved);
248             } else {
249                 if (this.activeZoneInstance) {
250                     this.unsetActiveZoneInstance();
251                 }
252                 this.selectComponentInstance(event.cyTarget[0].data().componentInstance);
253             }
254         }
255     }
256
257     private registerCytoscapeGraphEvents() {
258
259         this._cy.on('addedgemouseup', (event, data) => {
260             const connectRelationModel: ConnectRelationModel = this.compositionGraphLinkUtils.onLinkDrawn(this._cy, data.source, data.target);
261             if (connectRelationModel != null) {
262                 this.connectionWizardService.setRelationMenuDirectiveObj(connectRelationModel);
263                 this.connectionWizardService.selectedMatch = null;
264
265                 const steps: StepModel[] = [];
266                 const fromNodeName: string = connectRelationModel.fromNode.componentInstance.name;
267                 const toNodeName: string = connectRelationModel.toNode.componentInstance.name;
268                 steps.push(new StepModel(fromNodeName, FromNodeStepComponent));
269                 steps.push(new StepModel(toNodeName, ToNodeStepComponent));
270                 steps.push(new StepModel('Properties', PropertiesStepComponent));
271                 steps.push(new StepModel('Operations', RelationshipOperationsStepComponent));
272                 const wizardTitle = 'Connect: ' + fromNodeName + ' to ' + toNodeName;
273                 const modalInstance = this.modalService.createMultiStepsWizard(wizardTitle, steps, this.createLinkFromMenu, ConnectionWizardHeaderComponent);
274                 modalInstance.instance.open();
275             }
276         });
277
278         this._cy.on('tapstart', 'node', (event: Cy.EventObject) => {
279             this.currentlyClickedNodePosition = angular.copy(event.cyTarget[0].position()); // update node position on drag
280         });
281
282         this._cy.on('drag', 'node', (event: Cy.EventObject) => {
283             if (event.cyTarget.data().componentSubType !== SdcElementType.POLICY && event.cyTarget.data().componentSubType !== SdcElementType.GROUP) {
284                 event.cyTarget.style({'overlay-opacity': 0.24});
285                 if (this.generalGraphUtils.isValidDrop(this._cy, event.cyTarget)) {
286                     event.cyTarget.style({'overlay-color': GraphColors.NODE_BACKGROUND_COLOR});
287                 } else {
288                     event.cyTarget.style({'overlay-color': GraphColors.NODE_OVERLAPPING_BACKGROUND_COLOR});
289                 }
290             }
291         });
292
293         this._cy.on('handlemouseover', (event, payload) => {
294             // no need to add opacity while we are dragging and hovering othe nodes- or if opacity was already calculated for these nodes
295             if (payload.node.grabbed() || this._cy.scratch('_edge_editation_highlights') === true) {
296                 return;
297             }
298
299             if (this.zoneTagMode) {
300                 this.zoneTagMode = this.zones[this.activeZoneInstance.type].getHoverTagModeId();
301                 return;
302             }
303
304             const nodesData = this.nodesGraphUtils.getAllNodesData(this._cy.nodes());
305             const nodesLinks = this.generalGraphUtils.getAllCompositionCiLinks(this._cy);
306             const instance = payload.node.data().componentInstance;
307             const filteredNodesData = this.matchCapabilitiesRequirementsUtils.findMatchingNodesToComponentInstance(instance, nodesData, nodesLinks);
308             this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, this._cy);
309             this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, this._cy, payload.node.data());
310
311             this._cy.scratch()._edge_editation_highlights = true;
312         });
313
314         this._cy.on('handlemouseout', () => {
315             if (this.zoneTagMode) {
316                 this.zoneTagMode = this.zones[this.activeZoneInstance.type].getTagModeId();
317                 return;
318             }
319             if (this._cy.scratch('_edge_editation_highlights') === true) {
320                 this._cy.removeScratch('_edge_editation_highlights');
321                 this._cy.emit('hidehandles');
322                 this.matchCapabilitiesRequirementsUtils.resetFadedNodes(this._cy);
323             }
324         });
325
326         this._cy.on('tapend', (event: Cy.EventObject) => {
327             this.onTapEnd(event);
328         });
329
330         this._cy.on('boxselect', 'node', (event: Cy.EventObject) => {
331             this.unsetActiveZoneInstance();
332             this.selectComponentInstance(event.cyTarget.data().componentInstance);
333         });
334
335         this._cy.on('canvasredraw', (event: Cy.EventObject) => {
336             if (this.zoneTagMode) {
337                 this.compositionGraphZoneUtils.showZoneTagIndications(this._cy, this.activeZoneInstance);
338             }
339         });
340
341         this._cy.on('handletagclick', (event: Cy.EventObject, eventData: any) => {
342             this.compositionGraphZoneUtils.handleTagClick(this._cy, this.activeZoneInstance, eventData.nodeId);
343         });
344     }
345
346     private initViewMode() {
347
348         if (this.isViewOnly()) {
349             // remove event listeners
350             this._cy.off('drag');
351             this._cy.off('handlemouseout');
352             this._cy.off('handlemouseover');
353             this._cy.off('canvasredraw');
354             this._cy.off('handletagclick');
355             this._cy.edges().unselectify();
356         }
357     }
358
359     private saveChangedCapabilityProperties = (): Promise<PropertyBEModel[]> => {
360         return new Promise<PropertyBEModel[]>((resolve) => {
361             const capabilityPropertiesBE: PropertyBEModel[] = this.connectionWizardService.changedCapabilityProperties.map((prop) => {
362                 prop.value = prop.getJSONValue();
363                 const propBE = new PropertyBEModel(prop);
364                 propBE.parentUniqueId = this.connectionWizardService.selectedMatch.relationship.relation.capabilityOwnerId;
365                 return propBE;
366             });
367             if (capabilityPropertiesBE.length > 0) {
368                 // if there are capability properties to update, then first update capability properties and then resolve promise
369                 this.componentInstanceService
370                     .updateInstanceCapabilityProperties(
371                         this.topologyTemplate,
372                         this.connectionWizardService.selectedMatch.toNode,
373                         this.connectionWizardService.selectedMatch.capability,
374                         capabilityPropertiesBE
375                     )
376                     .subscribe((response) => {
377                         console.log('Update resource instance capability properties response: ', response);
378                         this.connectionWizardService.changedCapabilityProperties = [];
379                         resolve(capabilityPropertiesBE);
380                     });
381             } else {
382                 // no capability properties to update, immediately resolve promise
383                 resolve(capabilityPropertiesBE);
384             }
385         });
386     }
387
388     private loadCompositionData = () => {
389         this.loaderService.activate();
390         this.topologyTemplateService.getComponentCompositionData(this.topologyTemplateId, this.topologyTemplateType).subscribe((response: ComponentGenericResponse) => {
391             if (this.topologyTemplateType === ComponentType.SERVICE) {
392                 this.compositionService.forwardingPaths = (response as ServiceGenericResponse).forwardingPaths;
393             }
394             this.compositionService.componentInstances = response.componentInstances;
395             this.compositionService.componentInstancesRelations = response.componentInstancesRelations;
396             this.compositionService.groupInstances = response.groupInstances;
397             this.compositionService.policies = response.policies;
398             this.loadGraphData();
399             this.loaderService.deactivate();
400         }, (error) => { this.loaderService.deactivate(); });
401     }
402
403     private loadGraph = () => {
404         const graphEl = this.elRef.nativeElement.querySelector('.sdc-composition-graph-wrapper');
405         this.initGraph(graphEl);
406         this.zones = this.compositionGraphZoneUtils.createCompositionZones();
407         this.registerCytoscapeGraphEvents();
408         this.registerCustomEvents();
409         this.initViewMode();
410     }
411
412     private initGraphNodes() {
413
414         setTimeout(() => {
415             const handles = new CytoscapeEdgeEditation();
416             handles.init(this._cy);
417             if (!this.isViewOnly()) { // Init nodes handle extension - enable dynamic links
418                 handles.initNodeEvents();
419                 handles.registerHandle(ComponentInstanceNodesStyle.getAddEdgeHandle());
420             }
421             handles.registerHandle(ComponentInstanceNodesStyle.getTagHandle());
422             handles.registerHandle(ComponentInstanceNodesStyle.getTaggedPolicyHandle());
423             handles.registerHandle(ComponentInstanceNodesStyle.getTaggedGroupHandle());
424         }, 0);
425
426         _.each(this.compositionService.componentInstances, (instance) => {
427             const compositionGraphNode: CompositionCiNodeBase = this.nodesFactory.createNode(instance);
428             this.commonGraphUtils.addComponentInstanceNodeToGraph(this._cy, compositionGraphNode);
429         });
430
431     }
432
433     private loadGraphData = () => {
434         this.initGraphNodes();
435         this.compositionGraphLinkUtils.initGraphLinks(this._cy, this.compositionService.componentInstancesRelations);
436         this.compositionGraphZoneUtils.initZoneInstances(this.zones);
437         setTimeout(() => { // Need setTimeout so that angular canvas changes will take effect before resize & center
438             this.generalGraphUtils.zoomAllWithMax(this._cy, 1);
439         });
440         this.componentInstanceNames = _.map(this._cy.nodes(), (node) => node.data('name'));
441     }
442
443     private initGraph(graphEl: JQuery) {
444
445         this._cy = cytoscape({
446             container: graphEl,
447             style: ComponentInstanceNodesStyle.getCompositionGraphStyle(),
448             zoomingEnabled: true,
449             maxZoom: 1.2,
450             minZoom: .1,
451             userZoomingEnabled: false,
452             userPanningEnabled: true,
453             selectionType: 'single',
454             boxSelectionEnabled: true,
455             autolock: this.isViewOnly(),
456             autoungrabify: this.isViewOnly()
457         });
458
459         // Testing Bridge that allows Cypress tests to select a component on canvas not via DOM
460         if (window.Cypress) {
461             window.testBridge = this.createCanvasTestBridge();
462         }
463     }
464
465     private createCanvasTestBridge(): any {
466         return {
467             selectComponentInstance: (componentName: string) => {
468                 const matchingNodesByName = this.nodesGraphUtils.getMatchingNodesByName(this._cy, componentName);
469                 const component = new ComponentInstance(matchingNodesByName.first().data().componentInstance);
470                 this.selectComponentInstance(component);
471             }
472         };
473     }
474
475     // -------------------------------------------- ZONES---------------------------------------------------------//
476     private zoneMinimizeToggle = (zoneType: ZoneInstanceType): void => {
477         this.zones[zoneType].minimized = !this.zones[zoneType].minimized;
478     }
479
480     private zoneInstanceModeChanged = (newMode: ZoneInstanceMode, instance: ZoneInstance, zoneId: ZoneInstanceType): void => {
481         if (this.zoneTagMode) { // we're in tag mode.
482             if (instance === this.activeZoneInstance && newMode === ZoneInstanceMode.NONE) { // we want to turn tag mode off.
483                 this.zoneTagMode = null;
484                 this.activeZoneInstance.mode = ZoneInstanceMode.SELECTED;
485                 this.compositionGraphZoneUtils.endCyTagMode(this._cy);
486                 this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CANVAS_TAG_END, instance);
487
488             }
489         } else {
490             // when active zone instance gets hover/none, don't actually change mode, just show/hide indications
491             if (instance !== this.activeZoneInstance || (instance === this.activeZoneInstance && newMode > ZoneInstanceMode.HOVER)) {
492                 instance.mode = newMode;
493             }
494
495             if (newMode === ZoneInstanceMode.NONE) {
496                 this.compositionGraphZoneUtils.hideZoneTagIndications(this._cy);
497                 if (this.zones[ZoneInstanceType.GROUP]) {
498                     this.compositionGraphZoneUtils.hideGroupZoneIndications(this.zones[ZoneInstanceType.GROUP].instances);
499                 }
500             }
501             if (newMode >= ZoneInstanceMode.HOVER) {
502                 this.compositionGraphZoneUtils.showZoneTagIndications(this._cy, instance);
503                 if (instance.type === ZoneInstanceType.POLICY && this.zones[ZoneInstanceType.GROUP]) {
504                     this.compositionGraphZoneUtils.showGroupZoneIndications(this.zones[ZoneInstanceType.GROUP].instances, instance);
505                 }
506             }
507             if (newMode >= ZoneInstanceMode.SELECTED) {
508                 this._cy.$('node:selected').unselect();
509                 if (this.activeZoneInstance && this.activeZoneInstance !== instance && newMode >= ZoneInstanceMode.SELECTED) {
510                     this.activeZoneInstance.mode = ZoneInstanceMode.NONE;
511                 }
512                 this.activeZoneInstance = instance;
513                 this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, instance);
514                 this.store.dispatch(new SetSelectedComponentAction({
515                     component: instance.instanceData,
516                     type: SelectedComponentType[ZoneInstanceType[instance.type]]
517                 }));
518             }
519             if (newMode === ZoneInstanceMode.TAG) {
520                 this.compositionGraphZoneUtils.startCyTagMode(this._cy);
521                 this.zoneTagMode = this.zones[zoneId].getTagModeId();
522                 this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CANVAS_TAG_START, zoneId);
523             }
524         }
525     }
526
527     private zoneInstanceTagged = (taggedInstance: ZoneInstance) => {
528         this.activeZoneInstance.addOrRemoveAssignment(taggedInstance.instanceData.uniqueId, ZoneInstanceAssignmentType.GROUPS);
529         const newHandle: string = this.compositionGraphZoneUtils.getCorrectHandleForNode(taggedInstance.instanceData.uniqueId, this.activeZoneInstance);
530         taggedInstance.showHandle(newHandle);
531     }
532
533     private unsetActiveZoneInstance = (): void => {
534         if (this.activeZoneInstance) {
535             this.activeZoneInstance.mode = ZoneInstanceMode.NONE;
536             this.activeZoneInstance = null;
537             this.zoneTagMode = null;
538         }
539     }
540
541     private selectComponentInstance = (componentInstance: ComponentInstance) => {
542         this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_NODE_SELECTED, componentInstance);
543         this.store.dispatch(new SetSelectedComponentAction({
544             component: componentInstance,
545             type: SelectedComponentType.COMPONENT_INSTANCE
546         }));
547     }
548
549     private selectTopologyTemplate = () => {
550         this.store.dispatch(new SetSelectedComponentAction({
551             component: this.topologyTemplate,
552             type: SelectedComponentType.TOPOLOGY_TEMPLATE
553         }));
554     }
555
556     private zoneBackgroundClicked = (): void => {
557         if (!this.zoneTagMode && this.activeZoneInstance) {
558             this.unsetActiveZoneInstance();
559             this.selectTopologyTemplate();
560         }
561     }
562
563     private zoneAssignmentSaveStart = () => {
564         this.loaderService.activate();
565     }
566
567     private zoneAssignmentSaveComplete = (success: boolean) => {
568         this.loaderService.deactivate();
569         if (!success) {
570             this.notificationService.push(new NotificationSettings('error', 'Update Failed', 'Error'));
571         }
572     }
573
574     private deleteZoneInstance = (deletedInstance: UIZoneInstanceObject) => {
575         if (deletedInstance.type === ZoneInstanceType.POLICY) {
576             this.compositionService.policies = this.compositionService.policies.filter((policy) => policy.uniqueId !== deletedInstance.uniqueId);
577         } else if (deletedInstance.type === ZoneInstanceType.GROUP) {
578             this.compositionService.groupInstances = this.compositionService.groupInstances.filter((group) => group.uniqueId !== deletedInstance.uniqueId);
579         }
580         // remove it from zones
581         this.zones[deletedInstance.type].removeInstance(deletedInstance.uniqueId);
582         if (deletedInstance.type === ZoneInstanceType.GROUP && !_.isEmpty(this.zones[ZoneInstanceType.POLICY])) {
583             this.compositionGraphZoneUtils.updateTargetsOrMembersOnCanvasDelete(deletedInstance.uniqueId, [this.zones[ZoneInstanceType.POLICY]], ZoneInstanceAssignmentType.GROUPS);
584         }
585         this.selectTopologyTemplate();
586     }
587     // -------------------------------------------------------------------------------------------------------------//
588
589     private registerCustomEvents() {
590
591         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, (groupInstance: GroupInstance) => {
592             this.compositionGraphZoneUtils.findAndUpdateZoneInstanceData(this.zones, groupInstance);
593             this.notificationService.push(new NotificationSettings('success', 'Group Updated', 'Success'));
594         });
595
596         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, (policyInstance: PolicyInstance) => {
597             this.compositionGraphZoneUtils.findAndUpdateZoneInstanceData(this.zones, policyInstance);
598             this.notificationService.push(new NotificationSettings('success', 'Policy Updated', 'Success'));
599         });
600
601         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, (leftPaletteComponent: LeftPaletteComponent) => {
602             this.compositionGraphPaletteUtils.onComponentHoverIn(leftPaletteComponent, this._cy);
603         });
604
605         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_ADD_ZONE_INSTANCE_FROM_PALETTE,
606             (component: TopologyTemplate, paletteComponent: LeftPaletteComponent, startPosition: Point) => {
607
608                 const zoneType: ZoneInstanceType = this.compositionGraphZoneUtils.getZoneTypeForPaletteComponent(paletteComponent.categoryType);
609                 this.compositionGraphZoneUtils.showZone(this.zones[zoneType]);
610
611                 this.loaderService.activate();
612                 this.compositionGraphZoneUtils.createZoneInstanceFromLeftPalette(zoneType, paletteComponent.type).subscribe((zoneInstance: ZoneInstance) => {
613                     this.loaderService.deactivate();
614                     this.compositionGraphZoneUtils.addInstanceToZone(this.zones[zoneInstance.type], zoneInstance, true);
615                     this.compositionGraphZoneUtils.createPaletteToZoneAnimation(startPosition, zoneType, zoneInstance);
616                 }, (error) => {
617                     this.loaderService.deactivate();
618                 });
619             });
620
621         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT, () => {
622             this._cy.emit('hidehandles');
623             this.matchCapabilitiesRequirementsUtils.resetFadedNodes(this._cy);
624         });
625
626         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_START, (dragElement, dragComponent) => {
627             this.dragElement = dragElement;
628             this.dragComponent = ComponentInstanceFactory.createComponentInstanceFromComponent(dragComponent, this.workspaceService.metadata.categories[0].useServiceSubstitutionForNestedServices);
629         });
630
631         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_ACTION, (position: Point) => {
632             const draggedElement = document.getElementById('draggable_element');
633             draggedElement.className = this.compositionGraphPaletteUtils.isDragValid(this._cy, position) ? 'valid-drag' : 'invalid-drag';
634         });
635
636         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DROP, (event: DndDropEvent) => {
637             this.onDrop(event);
638         });
639
640         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_NAME_CHANGED, (component: ComponentInstance) => {
641             const selectedNode = this._cy.getElementById(component.uniqueId);
642             selectedNode.data().componentInstance.name = component.name;
643             selectedNode.data('name', component.name); // used for tooltip
644             selectedNode.data('displayName', selectedNode.data().getDisplayName()); // abbreviated
645         });
646
647         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_REQUIREMENT_EXTERNAL_CHANGED, (uniqueId:string, requirement:Requirement) => {
648             this._cy.getElementById(uniqueId).data().componentInstance.requirements[requirement.capability]
649                 .find(r => r.uniqueId === requirement.uniqueId).external = requirement.external;
650         });
651
652         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, (componentInstanceId: string) => {
653             const nodeToDelete = this._cy.getElementById(componentInstanceId);
654             this.nodesGraphUtils.deleteNode(this._cy, this.topologyTemplate, nodeToDelete);
655             this.selectTopologyTemplate();
656         });
657
658         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE, (deletedInstance: UIZoneInstanceObject) => {
659             this.deleteZoneInstance(deletedInstance);
660         });
661
662         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, (componentInstanceId: string) => {
663             if (!_.isEmpty(this.zones)) {
664                 this.compositionGraphZoneUtils.updateTargetsOrMembersOnCanvasDelete(componentInstanceId, this.zones, ZoneInstanceAssignmentType.COMPONENT_INSTANCES);
665             }
666         });
667
668         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_EDGE, (releaseLoading: boolean, linksToDelete: Cy.CollectionEdges) => {
669             this.compositionGraphLinkUtils.deleteLink(this._cy, this.topologyTemplate, releaseLoading, linksToDelete);
670         });
671
672         this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_VERSION_CHANGED, (component: ComponentInstance) => {
673             // Remove everything from graph and reload it all
674             this._cy.elements().remove();
675             this.loadCompositionData();
676             setTimeout(() => { this._cy.getElementById(component.uniqueId).select(); }, 1000);
677             this.selectComponentInstance(component);
678         });
679         this.eventListenerService.registerObserverCallback(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE, (ischecked: boolean) => {
680             if (ischecked) {
681                 this._cy.$('node:selected').addClass('dependent');
682             } else {
683                 // due to defect in cytoscape, just changing the class does not replace the icon, and i need to revert to original icon with no markings.
684                 this._cy.$('node:selected').removeClass('dependent');
685                 this._cy.$('node:selected').style({'background-image': this._cy.$('node:selected').data('originalImg')});
686             }
687         });
688     }
689     private createLinkFromMenu = (): void => {
690         this.saveChangedCapabilityProperties().then(() => {
691             this.compositionGraphLinkUtils.createLinkFromMenu(this._cy, this.connectionWizardService.selectedMatch);
692         });
693     }
694
695     private deleteRelation = (link: Cy.CollectionEdges) => {
696         // if multiple edges selected, delete the VL itself so edges get deleted automatically
697         if (this._cy.$('edge:selected').length > 1) {
698             this.nodesGraphUtils.deleteNode(this._cy, this.topologyTemplate, this._cy.$('node:selected'));
699         } else {
700             this.compositionGraphLinkUtils.deleteLink(this._cy, this.topologyTemplate, true, link);
701         }
702     }
703
704     private viewRelation = (link: Cy.CollectionEdges) => {
705
706         const linkData = link.data();
707         const sourceNode: CompositionCiNodeBase = link.source().data();
708         const targetNode: CompositionCiNodeBase = link.target().data();
709         const relationship: Relationship = linkData.relation.relationships[0];
710
711         this.compositionGraphLinkUtils.getRelationRequirementCapability(relationship, sourceNode.componentInstance, targetNode.componentInstance).then((objReqCap) => {
712             const capability = objReqCap.capability;
713             const requirement = objReqCap.requirement;
714
715             this.connectionWizardService.connectRelationModel = new ConnectRelationModel(sourceNode, targetNode, []);
716             this.connectionWizardService.selectedMatch = new Match(requirement, capability, true, linkData.source, linkData.target);
717             this.connectionWizardService.selectedMatch.relationship = relationship;
718
719             const title = `Connection Properties`;
720             const saveButton: ButtonModel = new ButtonModel('Save', 'blue', () => {
721                 this.saveChangedCapabilityProperties().then(() => {
722                     this.modalService.closeCurrentModal();
723                 });
724             });
725             const cancelButton: ButtonModel = new ButtonModel('Cancel', 'white', () => {
726                 this.modalService.closeCurrentModal();
727             });
728             const modal = new ModalModel('xl', title, '', [saveButton, cancelButton]);
729             const modalInstance = this.modalService.createCustomModal(modal);
730             this.modalService.addDynamicContentToModal(modalInstance, ConnectionPropertiesViewComponent);
731             modalInstance.instance.open();
732
733             new Promise((resolve) => {
734                 if (!this.connectionWizardService.selectedMatch.capability.properties) {
735                     this.componentInstanceService.getInstanceCapabilityProperties(this.topologyTemplateType, this.topologyTemplateId, linkData.target, capability)
736                         .subscribe(() => {
737                             resolve();
738                         }, () => { /* do nothing */ });
739                 } else {
740                     resolve();
741                 }
742             }).then(() => {
743                 this.modalService.addDynamicContentToModal(modalInstance, ConnectionPropertiesViewComponent);
744             });
745         }, () => { /* do nothing */ });
746     }
747
748     private openModifyLinkMenu = (linkMenuObject: LinkMenu, $event) => {
749
750         const menuConfig: menuItem[] = [{
751             text: 'View',
752             iconName: 'eye-o',
753             iconType: 'common',
754             iconMode: 'secondary',
755             iconSize: 'small',
756             type: '',
757             action: () => this.viewRelation(linkMenuObject.link as Cy.CollectionEdges)
758         }];
759
760         if (!this.isViewOnly()) {
761             menuConfig.push({
762                 text: 'Delete',
763                 iconName: 'trash-o',
764                 iconType: 'common',
765                 iconMode: 'secondary',
766                 iconSize: 'small',
767                 type: '',
768                 action: () => this.deleteRelation(linkMenuObject.link as Cy.CollectionEdges)
769             });
770         }
771         this.simplePopupMenuService.openBaseMenu(menuConfig, {
772             x: $event.originalEvent.x,
773             y: $event.originalEvent.y
774         });
775     }
776
777 }