summaryrefslogtreecommitdiffstats
path: root/catalog-ui/src/app/ng2/pages/composition/graph
diff options
context:
space:
mode:
authorys9693 <ys9693@att.com>2020-01-19 13:50:02 +0200
committerOfir Sonsino <ofir.sonsino@intl.att.com>2020-01-22 12:33:31 +0000
commit16a9fce0e104a38371a9e5a567ec611ae3fc7f33 (patch)
tree03a2aff3060ddb5bc26a90115805a04becbaffc9 /catalog-ui/src/app/ng2/pages/composition/graph
parentaa83a2da4f911c3ac89318b8e9e8403b072942e1 (diff)
Catalog alignment
Issue-ID: SDC-2724 Signed-off-by: ys9693 <ys9693@att.com> Change-Id: I52b4aacb58cbd432ca0e1ff7ff1f7dd52099c6fe
Diffstat (limited to 'catalog-ui/src/app/ng2/pages/composition/graph')
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.html23
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.less42
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.ts25
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.module.ts30
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/__snapshots__/zone-container.component.spec.ts.snap35
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.html30
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.less62
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.spec.ts46
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.ts35
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.html27
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.less135
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.spec.ts132
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.ts128
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zones-module.ts15
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/common/common-graph-utils.ts304
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/common/image-creator.service.ts92
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.spec.ts37
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.ts362
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/common/style/module-node-style.ts103
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.html57
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.less93
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.spec.ts354
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts768
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.module.ts55
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.html20
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.less4
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.ts10
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.html52
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.less53
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts37
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts43
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.spec.ts85
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.ts58
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/__snapshots__/from-node-step.component.spec.ts.snap12
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.html22
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.spec.ts114
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.ts44
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.html28
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.less15
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.ts68
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/__snapshots__/to-node-step.component.spec.ts.snap14
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.html22
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.spec.ts71
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.ts65
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/__snapshots__/link-row.component.spec.ts.snap29
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.html61
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.less21
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.spec.ts478
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.ts104
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link.model.ts36
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.html55
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.less45
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.ts149
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.module.ts25
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.html27
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.less24
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.ts142
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.module.ts22
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.html21
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.less24
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.ts70
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.module.ts17
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-general-utils.ts268
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-links-utils.ts342
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.spec.ts158
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.ts202
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-palette-utils.ts233
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-service-path-utils.ts148
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-zone-utils.ts204
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/index.ts29
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.spec.ts342
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.ts196
72 files changed, 7299 insertions, 0 deletions
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.html
new file mode 100644
index 0000000000..a8645dc5f0
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.html
@@ -0,0 +1,23 @@
+<div class="canvas-search-component" [ngClass]="{'results-shown': autoCompleteResults.length}"
+ [class.canvas-search-visible]="autoCompleteValues && autoCompleteValues.length" [attr.data-tests-id]="testId">
+ <div class="canvas-search-bar-container" [attr.data-tests-id]="testId"
+ [class.active]="searchQuery && searchQuery.length">
+ <sdc-search-bar class="canvas-search-bar"
+ [placeHolder]="placeholder"
+ (onSearchClicked)="onSearchClicked($event)"
+ [size]="'medium'"
+ [value]="searchQuery"
+ (valueChange)="onSearchQueryChanged($event)">
+ </sdc-search-bar>
+ <svg-icon class="canvas-clear-search"
+ [name]="'close'"
+ [clickable]="true"
+ [mode]="'secondary'"
+ [size]="'small'"
+ (click)="onClearSearch()">
+ </svg-icon>
+ </div>
+ <dropdown-results *ngIf="autoCompleteResults && autoCompleteResults.length" [options]="autoCompleteResults"
+ (onItemSelected)="onItemSelected($event)"></dropdown-results>
+</div>
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.less
new file mode 100644
index 0000000000..247f2a3913
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.less
@@ -0,0 +1,42 @@
+.canvas-search-component {
+
+ .canvas-search-bar-container {
+ display:flex;
+ border-radius: 4px;
+ align-items: center;
+ box-shadow: 0px 2px 3.88px 0.12px rgba(0, 0, 0, 0.29);
+
+ /deep/.sdc-search-bar .search-bar-container .search-button {
+ border: solid 1px #d2d2d2;
+ }
+
+ /deep/.sdc-input__input {
+ width: 250px;
+ transition: all 0.4s;
+ }
+
+ .canvas-clear-search {
+ position: absolute;
+ right: 45px;
+ }
+ }
+
+ &:not(:hover):not(.canvas-search-visible):not(.active) {
+ border-radius: 0;
+ box-shadow: none;
+
+ /deep/.sdc-input__input:not(:focus) {
+ border: none;
+ padding: 0px;
+ width: 0px;
+ }
+ .canvas-clear-search {
+ display: none;
+ }
+ }
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.ts
new file mode 100644
index 0000000000..c1a45a9a4b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.ts
@@ -0,0 +1,25 @@
+import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {AutoCompleteComponent} from "onap-ui-angular/dist/autocomplete/autocomplete.component";
+
+@Component({
+ selector: 'canvas-search',
+ templateUrl: './canvas-search.component.html',
+ styleUrls: ['./canvas-search.component.less']
+})
+export class CanvasSearchComponent extends AutoCompleteComponent {
+
+ @Output() public searchButtonClicked: EventEmitter<string> = new EventEmitter<string>();
+ @Output() public onSelectedItem: EventEmitter<string> = new EventEmitter<string>();
+
+ public onSearchClicked = (searchText:string)=> {
+ this.searchButtonClicked.emit(searchText);
+ }
+
+ public onItemSelected = (selectedItem) => {
+ this.searchQuery = selectedItem.value;
+ this.autoCompleteResults = [];
+ this.searchButtonClicked.emit(this.searchQuery);
+ this.onSelectedItem.emit(selectedItem);
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.module.ts
new file mode 100644
index 0000000000..6df06067a6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.module.ts
@@ -0,0 +1,30 @@
+import {SdcUiComponentsModule} from "onap-ui-angular";
+import { NgModule } from "@angular/core";
+import {CanvasSearchComponent} from "./canvas-search.component";
+import {CommonModule} from "@angular/common";
+import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
+import {HttpClientModule} from "@angular/common/http";
+import {BrowserModule} from "@angular/platform-browser";
+import {AutocompletePipe} from "onap-ui-angular/dist/autocomplete/autocomplete.pipe";
+
+@NgModule({
+ declarations: [
+ CanvasSearchComponent
+ ],
+ imports: [
+ CommonModule,
+ BrowserModule,
+ HttpClientModule,
+ BrowserAnimationsModule,
+ SdcUiComponentsModule,
+ ],
+ exports: [
+ CanvasSearchComponent
+ ],
+ entryComponents: [
+ CanvasSearchComponent
+ ],
+ providers: [AutocompletePipe]
+})
+export class CanvasSearchModule {
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/__snapshots__/zone-container.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/__snapshots__/zone-container.component.spec.ts.snap
new file mode 100644
index 0000000000..d4e2a7a359
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/__snapshots__/zone-container.component.spec.ts.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ZoneContainerComponent should match current snapshot of palette element component 1`] = `
+<zone-container
+ backgroundClick={[Function EventEmitter]}
+ backgroundClicked={[Function Function]}
+ minimize={[Function EventEmitter]}
+ unminifyZone={[Function Function]}
+>
+ <div>
+ <div
+ class="sdc-canvas-zone__header"
+ >
+ <div
+ class="sdc-canvas-zone__title"
+ >
+
+ <span
+ class="sdc-canvas-zone__counter"
+ >
+
+ </span>
+ </div>
+ <span
+ class="sdc-canvas-zone__state-button"
+ >
+ –
+ </span>
+ </div>
+ <div
+ class="sdc-canvas-zone__container"
+ />
+ </div>
+</zone-container>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.html
new file mode 100644
index 0000000000..d6343a4a4f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.html
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2018 AT&T Intellectual Property. All rights reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+
+<div class="sdc-canvas-zone {{class}}-zone" [class.minimized]="minimized" [class.hidden]="!visible"
+ (click)="backgroundClicked()">
+ <div class="sdc-canvas-zone__header" (click)="unminifyZone(); $event.stopPropagation();">
+ <div class="sdc-canvas-zone__title">{{title}}
+ <span class="sdc-canvas-zone__counter">{{count}}</span>
+ </div>
+ <span class="sdc-canvas-zone__state-button">&ndash;</span>
+ </div>
+ <div class="sdc-canvas-zone__container" #scrollDiv>
+ <ng-content></ng-content>
+ </div>
+</div>
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.less
new file mode 100644
index 0000000000..827786cc49
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.less
@@ -0,0 +1,62 @@
+.sdc-canvas-zone {
+ width: 285px;
+ max-height:186px;
+ display:flex;
+ flex-direction:column;
+ color:white;
+ font-family:OpenSans-Regular, sans-serif;
+ transition: width .2s ease-in-out, max-height .2s ease-in-out .1s;
+ position:relative;
+ bottom:0px;
+ margin-right: 5px;
+
+ .sdc-canvas-zone__header {
+ background: #5A5A5A;
+ border-radius: 2px 2px 0 0;
+ padding: 5px 10px;
+ display:flex;
+ justify-content: space-between;
+ font-size: 14px;
+ text-transform:uppercase;
+ .sdc-canvas-zone__state-button {
+ font-weight:bold;
+ cursor:pointer;
+ }
+ }
+
+ .sdc-canvas-zone__container {
+ padding:5px;
+ background-color: #5A5A5A;
+ opacity:0.9;
+ flex: 1;
+ display:flex;
+ flex-direction: row;
+ align-items: flex-start;
+ flex-wrap:wrap;
+ overflow-y:auto;
+ min-height: 80px;
+ max-height: 170px;
+ }
+
+
+ &.minimized {
+ max-height:30px;
+ width:120px;
+ cursor:pointer;
+
+ .sdc-canvas-zone__state-button {
+ display:none;
+ }
+ .sdc-canvas-zone__container {
+ flex: 0 0 0;
+ min-height: 0;
+ padding: 0;
+ overflow-y:hidden;
+ transition: min-height .2s ease-in-out .2s;
+ transition: padding .1s ease-in-out 0s;
+ }
+ }
+ &.hidden {
+ display:none;
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.spec.ts
new file mode 100644
index 0000000000..c432054492
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.spec.ts
@@ -0,0 +1,46 @@
+import {async, ComponentFixture} from '@angular/core/testing';
+import {By} from '@angular/platform-browser';
+import {ConfigureFn, configureTests} from '../../../../../../jest/test-config.helper';
+import 'jest-dom/extend-expect';
+import {ZoneInstanceType} from '../../../../../../app/models/graph/zones/zone-instance';
+import {ZoneContainerComponent} from './zone-container.component';
+
+
+describe('ZoneContainerComponent', () => {
+ let fixture: ComponentFixture<ZoneContainerComponent>;
+
+ beforeEach(
+ async(() => {
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [ZoneContainerComponent]
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(ZoneContainerComponent);
+ });
+ })
+ );
+
+
+ it('should match current snapshot of palette element component', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+ it('should have a group-zone class when the ZoneInstanceType is GROUP',
+ () => {
+ fixture.componentInstance.type = ZoneInstanceType.GROUP;
+ fixture.detectChanges();
+ const compiled = fixture.debugElement.query(By.css('.sdc-canvas-zone'));
+ expect(compiled.nativeElement).toHaveClass('group-zone');
+ });
+
+ it('should have a policy-zone class when the ZoneInstanceType is POLICY',
+ () => {
+ fixture.componentInstance.type = ZoneInstanceType.POLICY;
+ fixture.detectChanges();
+ const compiled = fixture.debugElement.query(By.css('.sdc-canvas-zone'));
+ expect(compiled.nativeElement).toHaveClass('policy-zone');
+ });
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.ts
new file mode 100644
index 0000000000..4757c1f36d
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.ts
@@ -0,0 +1,35 @@
+import { Component, Input, Output, ViewEncapsulation, EventEmitter, OnInit } from '@angular/core';
+import { ZoneInstanceType } from 'app/models/graph/zones/zone-instance';
+
+@Component({
+ selector: 'zone-container',
+ templateUrl: './zone-container.component.html',
+ styleUrls: ['./zone-container.component.less'],
+ encapsulation: ViewEncapsulation.None
+})
+
+export class ZoneContainerComponent implements OnInit {
+ @Input() title:string;
+ @Input() type:ZoneInstanceType;
+ @Input() count:number;
+ @Input() visible:boolean;
+ @Input() minimized:boolean;
+ @Output() minimize: EventEmitter<any> = new EventEmitter<any>();
+ @Output() backgroundClick: EventEmitter<void> = new EventEmitter<void>();
+ private class:string;
+
+ constructor() {}
+
+ ngOnInit() {
+ this.class = ZoneInstanceType[this.type].toLowerCase();
+ }
+
+ private unminifyZone = () => {
+ this.minimize.emit();
+ }
+
+ private backgroundClicked = () => {
+ this.backgroundClick.emit();
+ }
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.html
new file mode 100644
index 0000000000..d97be69e34
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.html
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright (C) 2018 AT&T Intellectual Property. All rights reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+
+<div #currentComponent class="zone-instance mode-{{zoneInstance.mode}}" [class.locked]="activeInstanceMode > MODE.HOVER"
+ [class.hiding]="hidden"
+ (mouseenter)="setMode(MODE.HOVER)" (mouseleave)="setMode(MODE.NONE)" (click)="setMode(MODE.SELECTED, $event)">
+ <div class="zone-instance__body" sdc-tooltip tooltip-text="{{zoneInstance.instanceData.name}}" [attr.data-tests-id]="zoneInstance.instanceData.name">
+ <div *ngIf="zoneInstance.handle" class="target-handle {{zoneInstance.handle}}"
+ (click)="tagHandleClicked($event)"></div>
+ <div *ngIf="!isViewOnly" class="zone-instance__handle" (click)="setMode(MODE.TAG, $event)">+</div>
+ <div class="zone-instance__body-content">{{zoneInstance.assignments.length || defaultIconText}}</div>
+ </div>
+ <div class="zone-instance__name">{{zoneInstance.instanceData.name}}</div>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.less
new file mode 100644
index 0000000000..c34b8e149a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.less
@@ -0,0 +1,135 @@
+@import '../../../../../../../assets/styles/variables';
+
+.zone-instance {
+
+ width:76px;
+ margin:5px;
+ opacity:1;
+
+ .zone-instance__handle {
+ display:none;
+ position:absolute;
+ left: 31px;
+ top: 8px;
+ width:22px;
+ height:22px;
+ cursor:pointer;
+ border: solid @main_color_p 1px;
+ border-radius: 2px;
+ text-align: center;
+ font-weight:bold;
+ }
+
+ .zone-instance__body {
+ position:relative;
+ margin:0 auto;
+ width:43px;
+ height:43px;
+ display:flex;
+ padding:3px;
+ }
+
+ .zone-instance__body-content {
+ border-radius: 2px;
+ flex:1;
+ color:@main_color_p;
+ font-size:16px;
+ text-align:center;
+ display:flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow:none;
+ transition:box-shadow 5s;
+ }
+
+ .zone-instance__name {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ text-align:center;
+ }
+ /* Dynamic classes below */
+
+ .target-handle {
+ position:absolute;
+ width:18px;
+ height:18px;
+ display:block;
+ top: -4px;
+ right: -6px;
+ background-size: 100% 100%;
+ cursor: url("../../../../../../../assets/styles/images/canvas-tagging-icons/policy_2.svg"), pointer;
+
+ &.tagged-policy {
+ background-image: url('../../../../../../../assets/styles/images/canvas-tagging-icons/policy_added.svg');
+ }
+
+ &.tag-available {
+ background-image: url('../../../../../../../assets/styles/images/canvas-tagging-icons/indication.svg');
+ }
+ }
+
+
+ &.mode-1, &.mode-2, &.mode-3 { //hover, selected, tag
+ .zone-instance__body {
+ border:solid 2px;
+ border-radius: 2px;
+ padding:2px;
+ cursor:pointer;
+ }
+ }
+
+ &.mode-1, &.mode-2:hover{
+ .zone-instance__handle{
+ display:block;
+ }
+ }
+
+ &.locked {
+ cursor: inherit;
+ }
+
+ &.hiding {
+ opacity:0;
+ .zone-instance__body-content {
+ box-shadow: #CCC 0px 0px 15px;
+ }
+ }
+
+
+ &.mode-3 .zone-instance__handle {
+ width:24px;
+ height:24px;
+ right:-6px;
+ top:7px;
+ display:block;
+ background-image: linear-gradient(-140deg, #009E98 0%, #97D648 100%);
+ border: 2px solid @main_color_p;
+ border-radius: 2px;
+ box-shadow: inset 2px -2px 3px 0 #007A3E;
+ }
+
+}
+.sdc-canvas-zone.group-zone {
+ .zone-instance__handle {
+ background-color:@main_color_a;
+ }
+ .zone-instance__body {
+ border-color:@main_color_a;
+ .zone-instance__body-content {
+ background: @main_color_a;
+ }
+ }
+}
+
+.sdc-canvas-zone.policy-zone {
+ .zone-instance__handle {
+ background-color:@main_color_r;
+ }
+ .zone-instance__body {
+ border-color:@main_color_r;
+ .zone-instance__body-content {
+ background: @main_color_r;
+ }
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.spec.ts
new file mode 100644
index 0000000000..f5a5f6f546
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.spec.ts
@@ -0,0 +1,132 @@
+import { ComponentFixture, TestBed, async } from '@angular/core/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { SimpleChanges } from '@angular/core';
+import { PoliciesService } from 'app/ng2/services/policies.service';
+import { GroupsService } from 'app/ng2/services/groups.service';
+import { EventListenerService } from 'app/services';
+import { Store } from '@ngxs/store';
+import { CompositionService } from 'app/ng2/pages/composition/composition.service';
+import { ZoneInstanceComponent } from './zone-instance.component';
+import { ZoneInstanceType, ZoneInstance, ZoneInstanceMode, ZoneInstanceAssignmentType, IZoneInstanceAssignment } from "app/models";
+import { PolicyInstance } from "app/models/graph/zones/policy-instance";
+import { Subject, of } from 'rxjs';
+import { _throw } from 'rxjs/observable/throw';
+
+describe('ZoneInstanceComponent', () => {
+ let component: ZoneInstanceComponent;
+ let fixture: ComponentFixture<ZoneInstanceComponent>;
+
+ let createPolicyInstance = () => {
+ let policy = new PolicyInstance();
+ policy.targets = {COMPONENT_INSTANCES: [], GROUPS: []};
+ return new ZoneInstance(policy, '', '');
+ }
+
+ beforeEach(() => {
+ const policiesServiceStub = {updateZoneInstanceAssignments : jest.fn()};
+ const groupsServiceStub = {};
+ const eventListenerServiceStub = {};
+ const storeStub = {};
+ const compositionServiceStub = {};
+ TestBed.configureTestingModule({
+ schemas: [NO_ERRORS_SCHEMA],
+ declarations: [ZoneInstanceComponent],
+ providers: [
+ { provide: PoliciesService, useValue: policiesServiceStub },
+ { provide: GroupsService, useValue: groupsServiceStub },
+ { provide: EventListenerService, useValue: eventListenerServiceStub },
+ { provide: Store, useValue: storeStub },
+ { provide: CompositionService, useValue: compositionServiceStub }
+ ]
+ }).compileComponents().then(() => {
+ fixture = TestBed.createComponent(ZoneInstanceComponent);
+ component = fixture.componentInstance;
+ });
+ });
+
+ it('can load instance', async((done) => {
+ component.zoneInstance = <ZoneInstance>{type : ZoneInstanceType.POLICY, instanceData: {name: 'test policy'}, assignments: []};
+ component.forceSave = new Subject<Function>();
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ }));
+
+
+ it('if another instance is already tagging, i cannot change my mode', ()=> {
+ component.zoneInstance = <ZoneInstance>{ mode: ZoneInstanceMode.NONE };
+ component.isActive = false;
+ component.activeInstanceMode = ZoneInstanceMode.TAG;
+ component.setMode(ZoneInstanceMode.SELECTED);
+ expect(component.zoneInstance.mode).toBe(ZoneInstanceMode.NONE);
+ });
+
+ it('if i am active(selected) and NOT in tag mode, I can set another mode', ()=> {
+ component.isActive = true;
+ component.zoneInstance = <ZoneInstance>{ mode: ZoneInstanceMode.SELECTED };
+ jest.spyOn(component.modeChange, 'emit');
+ component.setMode(ZoneInstanceMode.NONE);
+ expect(component.modeChange.emit).toHaveBeenCalledWith({instance: component.zoneInstance, newMode: ZoneInstanceMode.NONE });
+ });
+
+ it('if i am active and in tag mode and i try to set mode other than tag, I am not allowed', ()=> {
+ component.isActive = true;
+ component.zoneInstance = <ZoneInstance>{ mode: ZoneInstanceMode.TAG };
+ component.setMode(ZoneInstanceMode.SELECTED);
+ expect(component.zoneInstance.mode).toBe(ZoneInstanceMode.TAG);
+ });
+
+ it('if i am active and in tag mode and click tag again and no changes, does NOT call save, but DOES turn tagging off', ()=> {
+ component.isActive = true;
+ component.zoneInstance = createPolicyInstance();
+ component.zoneService = component.policiesService;
+ component.zoneInstance.mode = ZoneInstanceMode.TAG;
+ jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments');
+ jest.spyOn(component.modeChange, 'emit');
+
+ component.setMode(ZoneInstanceMode.TAG);
+
+ expect(component.zoneService.updateZoneInstanceAssignments).not.toHaveBeenCalled();
+ expect(component.modeChange.emit).toHaveBeenCalledWith({instance: component.zoneInstance, newMode: ZoneInstanceMode.NONE });
+
+ });
+ it('if i am active and in tag mode and click tag again and HAVE changes, calls save AND turns tagging off', ()=> {
+ component.isActive = true;
+ component.zoneInstance = createPolicyInstance();
+ component.zoneService = component.policiesService;
+ component.zoneInstance.mode = ZoneInstanceMode.TAG;
+ component.zoneInstance.assignments.push(<IZoneInstanceAssignment>{uniqueId: '123', type: ZoneInstanceAssignmentType.COMPONENT_INSTANCES});
+ jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments').mockReturnValue(of(true));
+ jest.spyOn(component.modeChange, 'emit');
+
+ component.setMode(ZoneInstanceMode.TAG);
+
+ expect(component.zoneService.updateZoneInstanceAssignments).toHaveBeenCalled();
+ expect(component.modeChange.emit).toHaveBeenCalledWith({instance: component.zoneInstance, newMode: ZoneInstanceMode.NONE });
+ });
+
+ it('on save error, temporary assignment list is reverted to saved assignments', ()=> {
+ component.isActive = true;
+ component.zoneInstance = createPolicyInstance();
+ component.zoneService = component.policiesService;
+ component.zoneInstance.mode = ZoneInstanceMode.TAG;
+ component.zoneInstance.assignments.push(<IZoneInstanceAssignment>{uniqueId: '123', type: ZoneInstanceAssignmentType.COMPONENT_INSTANCES});
+ jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments').mockReturnValue(_throw({status: 404}));
+
+ component.setMode(ZoneInstanceMode.TAG);
+
+ expect(component.zoneInstance.assignments.length).toEqual(0);
+ });
+
+ it('on save success, all changes are saved to zoneInstance.savedAssignments', ()=> {
+ component.isActive = true;
+ component.zoneInstance = createPolicyInstance();
+ component.zoneService = component.policiesService;
+ component.zoneInstance.mode = ZoneInstanceMode.TAG;
+ component.zoneInstance.assignments.push(<IZoneInstanceAssignment>{uniqueId: '123', type: ZoneInstanceAssignmentType.COMPONENT_INSTANCES});
+ jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments').mockReturnValue(of(true));
+
+ component.setMode(ZoneInstanceMode.TAG);
+
+ expect(component.zoneInstance.instanceData.getSavedAssignments().length).toEqual(1);
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.ts
new file mode 100644
index 0000000000..1b1363e576
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.ts
@@ -0,0 +1,128 @@
+import { Component, Input, Output, EventEmitter, ViewEncapsulation, OnInit, SimpleChange, ElementRef, ViewChild, SimpleChanges } from '@angular/core';
+import {
+ ZoneInstance, ZoneInstanceMode, ZoneInstanceType,
+ IZoneInstanceAssignment
+} from 'app/models/graph/zones/zone-instance';
+import { PoliciesService } from 'app/ng2/services/policies.service';
+import { GroupsService } from 'app/ng2/services/groups.service';
+import { IZoneService } from "app/models/graph/zones/zone";
+import { EventListenerService } from 'app/services';
+import { GRAPH_EVENTS } from 'app/utils';
+import { Subject } from 'rxjs';
+import { Store } from "@ngxs/store";
+import { CompositionService } from "app/ng2/pages/composition/composition.service";
+import { PolicyInstance } from "app/models";
+import {SelectedComponentType, SetSelectedComponentAction} from "../../../common/store/graph.actions";
+
+
+@Component({
+ selector: 'zone-instance',
+ templateUrl: './zone-instance.component.html',
+ styleUrls: ['./zone-instance.component.less'],
+ encapsulation: ViewEncapsulation.None
+})
+export class ZoneInstanceComponent implements OnInit {
+
+ @Input() zoneInstance:ZoneInstance;
+ @Input() defaultIconText:string;
+ @Input() isActive:boolean;
+ @Input() isViewOnly:boolean;
+ @Input() activeInstanceMode: ZoneInstanceMode;
+ @Input() hidden:boolean;
+ @Input() forceSave:Subject<Function>;
+ @Output() modeChange: EventEmitter<any> = new EventEmitter<any>();
+ @Output() assignmentSaveStart: EventEmitter<void> = new EventEmitter<void>();
+ @Output() assignmentSaveComplete: EventEmitter<boolean> = new EventEmitter<boolean>();
+ @Output() tagHandleClick: EventEmitter<ZoneInstance> = new EventEmitter<ZoneInstance>();
+ @ViewChild('currentComponent') currentComponent: ElementRef;
+ private MODE = ZoneInstanceMode;
+ private zoneService:IZoneService;
+
+ constructor(private policiesService:PoliciesService, private groupsService:GroupsService, private eventListenerService:EventListenerService, private compositionService:CompositionService, private store:Store){}
+
+ ngOnInit(){
+ if(this.zoneInstance.type == ZoneInstanceType.POLICY){
+ this.zoneService = this.policiesService;
+ } else {
+ this.zoneService = this.groupsService;
+ }
+ if(this.forceSave) {
+ this.forceSave.subscribe((afterSaveFunction:Function) => {
+ this.setMode(ZoneInstanceMode.TAG, null, afterSaveFunction);
+ })
+ }
+ }
+
+ ngOnChanges(changes:SimpleChanges) {
+ if(changes.hidden){
+ this.currentComponent.nativeElement.scrollIntoView({behavior: "smooth", block: "nearest", inline:"end"});
+ }
+ }
+
+ ngOnDestroy() {
+ if(this.forceSave) {
+ this.forceSave.unsubscribe();
+ }
+ }
+
+ private setMode = (mode:ZoneInstanceMode, event?:any, afterSaveCallback?:Function):void => {
+
+ if(event){ //prevent event from handle and then repeat event from zone instance
+ event.stopPropagation();
+ }
+
+ if(!this.isActive && this.activeInstanceMode === ZoneInstanceMode.TAG) {
+ return; //someone else is tagging. No events allowed
+ }
+
+ if(this.isActive && this.zoneInstance.mode === ZoneInstanceMode.TAG){
+ if(mode !== ZoneInstanceMode.TAG) {
+ return; //ignore all other events. The only valid option is saving changes.
+ }
+
+ let oldAssignments:Array<IZoneInstanceAssignment> = this.zoneInstance.instanceData.getSavedAssignments();
+ if(this.zoneInstance.isZoneAssignmentChanged(oldAssignments, this.zoneInstance.assignments)) {
+
+ this.assignmentSaveStart.emit();
+
+ this.zoneService.updateZoneInstanceAssignments(this.zoneInstance.parentComponentType, this.zoneInstance.parentComponentID, this.zoneInstance.instanceData.uniqueId, this.zoneInstance.assignments).subscribe(
+ (success) => {
+ this.zoneInstance.instanceData.setSavedAssignments(this.zoneInstance.assignments);
+
+ if(this.zoneInstance.instanceData instanceof PolicyInstance){
+ this.compositionService.updatePolicy(this.zoneInstance.instanceData);
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, this.zoneInstance.instanceData);
+ this.store.dispatch(new SetSelectedComponentAction({component: this.zoneInstance.instanceData, type: SelectedComponentType.POLICY}));
+ } else {
+ this.compositionService.updateGroup(this.zoneInstance.instanceData);
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.zoneInstance.instanceData);
+ this.store.dispatch(new SetSelectedComponentAction({component: this.zoneInstance.instanceData, type: SelectedComponentType.GROUP}));
+ }
+
+ this.assignmentSaveComplete.emit(true);
+ if(afterSaveCallback) afterSaveCallback();
+ }, (error) => {
+ this.zoneInstance.assignments = oldAssignments;
+ this.assignmentSaveComplete.emit(false);
+ });
+ } else {
+ if(afterSaveCallback) afterSaveCallback();
+ }
+ this.modeChange.emit({newMode: ZoneInstanceMode.NONE, instance: this.zoneInstance});
+ // this.store.dispatch(new unsavedChangesActions.RemoveUnsavedChange(this.zoneInstance.instanceData.uniqueId));
+
+
+ } else {
+ this.modeChange.emit({newMode: mode, instance: this.zoneInstance});
+ if(mode == ZoneInstanceMode.TAG){
+ // this.store.dispatch(new unsavedChangesActions.AddUnsavedChange(this.zoneInstance.instanceData.uniqueId));
+ }
+ }
+ }
+
+ private tagHandleClicked = (event:Event) => {
+ this.tagHandleClick.emit(this.zoneInstance);
+ event.stopPropagation();
+ };
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zones-module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zones-module.ts
new file mode 100644
index 0000000000..3287c01f5a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zones-module.ts
@@ -0,0 +1,15 @@
+import { NgModule } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { ZoneContainerComponent } from "./zone-container.component";
+import { ZoneInstanceComponent } from "./zone-instance/zone-instance.component";
+import { SdcUiComponentsModule } from "onap-ui-angular";
+
+@NgModule({
+ declarations: [ZoneContainerComponent, ZoneInstanceComponent],
+ imports: [CommonModule, SdcUiComponentsModule],
+ entryComponents: [ZoneContainerComponent, ZoneInstanceComponent],
+ exports: [ZoneContainerComponent, ZoneInstanceComponent],
+ providers: []
+})
+export class ZoneModules {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/common-graph-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/common-graph-utils.ts
new file mode 100644
index 0000000000..bfc540e97e
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/common-graph-utils.ts
@@ -0,0 +1,304 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+import * as _ from "lodash";
+import {
+ CommonNodeBase,
+ Relationship,
+ CompositionCiNodeBase
+} from "app/models";
+import {CompositionCiServicePathLink} from "app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link";
+import {Requirement, Capability} from "app/models";
+import {Injectable} from "@angular/core";
+
+
+
+@Injectable()
+export class CommonGraphUtils {
+
+ constructor() {
+
+ }
+
+ public safeApply = (scope:ng.IScope, fn:any) => { //todo remove to general utils
+ let phase = scope.$root.$$phase;
+ if (phase == '$apply' || phase == '$digest') {
+ if (fn && (typeof(fn) === 'function')) {
+ fn();
+ }
+ } else {
+ scope.$apply(fn);
+ }
+ };
+
+ /**
+ * Draw node on the graph
+ * @param cy
+ * @param compositionGraphNode
+ * @param position
+ * @returns {CollectionElements}
+ */
+ public addNodeToGraph(cy:Cy.Instance, compositionGraphNode:CommonNodeBase, position?:Cy.Position):Cy.CollectionElements {
+
+ let node = cy.add(<Cy.ElementDefinition> {
+ group: 'nodes',
+ position: position,
+ data: compositionGraphNode,
+ classes: compositionGraphNode.classes
+ });
+
+ this.initNodeTooltip(node);
+ return node;
+ };
+
+ /**
+ * The function will create a component instance node by the componentInstance position.
+ * If the node is UCPE the function will create all cp lan&wan for the ucpe
+ * @param cy
+ * @param compositionGraphNode
+ * @returns {Cy.CollectionElements}
+ */
+ public addComponentInstanceNodeToGraph(cy:Cy.Instance, compositionGraphNode:CompositionCiNodeBase):Cy.CollectionElements {
+
+ let nodePosition = {
+ x: +compositionGraphNode.componentInstance.posX,
+ y: +compositionGraphNode.componentInstance.posY
+ };
+
+ let node = this.addNodeToGraph(cy, compositionGraphNode, nodePosition);
+ return node;
+ };
+
+ /**
+ * Add service path link to graph - only draw the link
+ * @param cy
+ * @param link
+ */
+ public insertServicePathLinkToGraph = (cy:Cy.Instance, link:CompositionCiServicePathLink) => {
+ let linkElement = cy.add({
+ group: 'edges',
+ data: link,
+ classes: link.classes
+ });
+ this.initServicePathTooltip(linkElement, link);
+ };
+
+ /**
+ * Returns function for the link tooltip content
+ * @param {Relationship} linkRelation
+ * @param {Requirement} requirement
+ * @param {Capability} capability
+ * @returns {() => string}
+ * @private
+ */
+ private _getLinkTooltipContent(linkRelation:Relationship, requirement?:Requirement, capability?:Capability):string {
+ return '<div class="line">' +
+ '<span class="req-cap-label">R: </span>' +
+ '<span>' + (requirement ? requirement.getTitle() : linkRelation.relation.requirement) + '</span>' +
+ '</div>' +
+ '<div class="line">' +
+ '<div class="sprite-new link-tooltip-arrow"></div>' +
+ '<span class="req-cap-label">C: </span>' +
+ '<span>' + (capability ? capability.getTitle() : linkRelation.relation.capability) + '</span>' +
+ '</div>';
+ }
+
+ /**
+ * This function will init qtip tooltip on the link
+ * @param linkElement - the link we want the tooltip to apply on,
+ * @param link
+ * @param getLinkRequirementCapability
+ * link - the link obj
+ */
+ public initLinkTooltip(linkElement:Cy.CollectionElements, link:Relationship, getLinkRequirementCapability:Function) {
+ const content = () => this._getLinkTooltipContent(link); // base tooltip content without owner names
+ const render = (event, api) => {
+ // on render (called once at first show), get the link requirement and capability and change to full tooltip content (with owner names)
+ getLinkRequirementCapability().then((linkReqCap) => {
+ const fullContent = () => this._getLinkTooltipContent(link, linkReqCap.requirement, linkReqCap.capability);
+ api.set('content.text', fullContent);
+ });
+ };
+ linkElement.qtip(this.prepareInitTooltipData({content, events: {render}}));
+ };
+
+ /**
+ *
+ * @param linkElement
+ * @param link
+ */
+ public initServicePathTooltip(linkElement:Cy.CollectionElements, link:CompositionCiServicePathLink) {
+ let content = function () {
+ return '<div class="line">' +
+ '<div>' + link.pathName + '</div>' +
+ '</div>';
+ };
+ linkElement.qtip(this.prepareInitTooltipData({content}));
+ };
+
+ private prepareInitTooltipData(options?:Object) {
+ return _.merge({
+ position: {
+ my: 'top center',
+ at: 'bottom center',
+ adjust: {x: 0, y: 0},
+ effect: false
+ },
+ style: {
+ classes: 'qtip-dark qtip-rounded qtip-custom link-qtip',
+ tip: {
+ width: 16,
+ height: 8
+ }
+ },
+ show: {
+ event: 'mouseover',
+ delay: 1000
+ },
+ hide: {event: 'mouseout mousedown'},
+ includeLabels: true,
+ events: {}
+ }, options);
+ }
+
+ public HTMLCoordsToCytoscapeCoords(cytoscapeBoundingBox:Cy.Extent, mousePos:Cy.Position):Cy.Position {
+ return {x: mousePos.x + cytoscapeBoundingBox.x1, y: mousePos.y + cytoscapeBoundingBox.y1}
+ };
+
+
+ public getCytoscapeNodePosition = (cy:Cy.Instance, event:DragEvent | MouseEvent):Cy.Position => {
+ let targetOffset = $(event.target).offset();
+ if(event instanceof DragEvent) {
+ targetOffset = $('canvas').offset();
+ }
+
+ let x = (event.pageX - targetOffset.left) / cy.zoom();
+ let y = (event.pageY - targetOffset.top) / cy.zoom();
+
+ return this.HTMLCoordsToCytoscapeCoords(cy.extent(), {
+ x: x,
+ y: y
+ });
+ };
+
+
+ public getNodePosition(node:Cy.CollectionFirstNode):Cy.Position {
+ let nodePosition = node.relativePoint();
+ if (node.data().isUcpe) { //UCPEs use bounding box and not relative point.
+ nodePosition = {x: node.boundingbox().x1, y: node.boundingbox().y1};
+ }
+
+ return nodePosition;
+ }
+
+ /**
+ * Generic function that can be used for any html elements overlaid on canvas
+ * Returns the html position of a node on canvas, including left palette and header offsets. Option to pass in additional offset to add to return position.
+ * @param node
+ * @param additionalOffset
+ * @returns {Cy.Position}
+
+ public getNodePositionWithOffset = (node:Cy.CollectionFirstNode, additionalOffset?:Cy.Position): Cy.Position => {
+ if(!additionalOffset) additionalOffset = {x: 0, y:0};
+
+ let nodePosition = node.renderedPosition();
+ let posWithOffset:Cy.Position = {
+ x: nodePosition.x + GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET + additionalOffset.x,
+ y: nodePosition.y + GraphUIObjects.COMPOSITION_HEADER_OFFSET + additionalOffset.y
+ };
+ return posWithOffset;
+ };*/
+
+ /**
+ * return true/false if first node contains in second - this used in order to verify is node is entirely inside ucpe
+ * @param firstBox
+ * @param secondBox
+ * @returns {boolean}
+ */
+ public isFirstBoxContainsInSecondBox(firstBox:Cy.BoundingBox, secondBox:Cy.BoundingBox) {
+
+ return firstBox.x1 > secondBox.x1 && firstBox.x2 < secondBox.x2 && firstBox.y1 > secondBox.y1 && firstBox.y2 < secondBox.y2;
+
+ };
+
+ /**
+ *
+ * @param cy
+ * @param node
+ * @returns {Array}
+ */
+ public getLinkableNodes(cy:Cy.Instance, node:Cy.CollectionFirstNode):Array<CompositionCiNodeBase> {
+ let compatibleNodes = [];
+ _.each(cy.nodes(), (tempNode)=> {
+ if (this.nodeLocationsCompatible(node, tempNode)) {
+ compatibleNodes.push(tempNode.data());
+ }
+ });
+ return compatibleNodes;
+ }
+
+ /**
+ * Checks whether node locations are compatible in reference to UCPEs.
+ * Returns true if both nodes are in UCPE or both nodes out, or one node is UCPEpart.
+ * @param node1
+ * @param node2
+ */
+ public nodeLocationsCompatible(node1:Cy.CollectionFirstNode, node2:Cy.CollectionFirstNode) {
+ return (this.isFirstBoxContainsInSecondBox(node1.boundingbox(), node2.boundingbox()));
+ }
+
+ /**
+ * This function will init qtip tooltip on the node
+ * @param node - the node we want the tooltip to apply on
+ */
+ public initNodeTooltip(node:Cy.CollectionNodes) {
+
+ let opts = {
+ content: function () {
+ return this.data('name');
+ },
+ position: {
+ my: 'top center',
+ at: 'bottom center',
+ adjust: {x: 0, y: -5}
+ },
+ style: {
+ classes: 'qtip-dark qtip-rounded qtip-custom',
+ tip: {
+ width: 16,
+ height: 8
+ }
+ },
+ show: {
+ event: 'mouseover',
+ delay: 1000
+ },
+ hide: {event: 'mouseout mousedown'},
+ includeLabels: true
+ };
+
+ if (node.data().isUcpePart) { //fix tooltip positioning for UCPE-cps
+ opts.position.adjust = {x: 0, y: 20};
+ }
+
+ node.qtip(opts);
+ };
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/image-creator.service.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/image-creator.service.ts
new file mode 100644
index 0000000000..2be92c782b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/image-creator.service.ts
@@ -0,0 +1,92 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+'use strict';
+import {Injectable} from "@angular/core";
+
+export interface ICanvasImage {
+ src: string;
+ width: number
+ height: number;
+ x: number;
+ y: number;
+}
+
+@Injectable()
+export class ImageCreatorService {
+
+ private _canvas:HTMLCanvasElement;
+
+ constructor() {
+ this._canvas = <HTMLCanvasElement>$('<canvas>')[0];
+ this._canvas.setAttribute('style', 'display:none');
+
+ let body = document.getElementsByTagName('body')[0];
+ body.appendChild(this._canvas);
+ }
+
+ /**
+ * Create an image composed of different image layers
+ * @param canvasImages
+ * @param canvasWidth
+ * @param canvasHeight
+ * returns a PROMISE
+ */
+ getMultiLayerBase64Image(canvasImages: ICanvasImage[], canvasWidth?:number, canvasHeight?:number):Promise<string> {
+
+ var promise = new Promise<string>((resolve, reject) => {
+ if(canvasImages && canvasImages.length === 0){
+ return null;
+ }
+
+ //If only width was set, use it for height, otherwise use first canvasImage height
+ canvasHeight = canvasHeight || canvasImages[0].height;
+ canvasWidth = canvasWidth || canvasImages[0].width;
+
+ const images = [];
+ let imagesLoaded = 0;
+ const onImageLoaded = () => {
+ imagesLoaded++;
+ if(imagesLoaded < canvasImages.length){
+ return;
+ }
+ this._canvas.setAttribute('width', (canvasWidth * 4).toString());
+ this._canvas.setAttribute('height', (canvasHeight * 4).toString());
+ const canvasCtx = this._canvas.getContext('2d');
+ canvasCtx.scale(4,4);
+ canvasCtx.clearRect(0, 0, this._canvas.width, this._canvas.height);
+ images.forEach((image, index) => {
+ const canvasImage = canvasImages[index];
+ canvasCtx.drawImage(image, canvasImage.x, canvasImage.y, canvasImage.width, canvasImage.height);
+ });
+
+ let base64Image = this._canvas.toDataURL();
+ resolve(base64Image)
+ };
+ canvasImages.forEach(canvasImage => {
+ let image = new Image();
+ image.onload = onImageLoaded;
+ image.src = canvasImage.src;
+ images.push(image);
+ });
+ });
+
+ return promise;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.spec.ts
new file mode 100644
index 0000000000..54b3dbed24
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.spec.ts
@@ -0,0 +1,37 @@
+import {async} from "@angular/core/testing";
+import {ComponentInstanceNodesStyle} from "./component-instances-nodes-style";
+
+
+describe('component instance nodes style component', () => {
+
+ beforeEach(
+ async(() => {
+ const createElement = document.createElement.bind(document);
+ document.createElement = (tagName) => {
+ if (tagName === 'canvas') {
+ return {
+ getContext: () => ({
+ font: "",
+ measureText: (x) => ({width: x.length})
+ }),
+ };
+ }
+ return createElement(tagName);
+ };
+ })
+ );
+
+ it('verify getGraphDisplayName for String.length smaller than 67 chars', () => {
+ let inputString = 'SomeText';
+ let expectedRes = inputString;
+ let res = ComponentInstanceNodesStyle.getGraphDisplayName(inputString);
+ expect(res).toBe(expectedRes);
+ });
+
+ it('verify getGraphDisplayName for String.length greater than 67 chars', () => {
+ let inputString = 'AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFGGGGGGGGGG12345678';
+ let expectedRes = 'AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFF...';
+ let res = ComponentInstanceNodesStyle.getGraphDisplayName(inputString);
+ expect(res).toBe(expectedRes);
+ });
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.ts
new file mode 100644
index 0000000000..cc9cac16e6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.ts
@@ -0,0 +1,362 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+import { GraphColors, GraphUIObjects} from "app/utils/constants";
+import constant = require("lodash/constant");
+import {ImagesUrl} from "app/utils/constants";
+import {AngularJSBridge} from "app/services/angular-js-bridge-service";
+import { CanvasHandleTypes } from "app/utils";
+/**
+ * Created by obarda on 12/18/2016.
+ */
+export class ComponentInstanceNodesStyle {
+
+ public static getCompositionGraphStyle = ():Array<Cy.Stylesheet> => {
+ return [
+ {
+ selector: 'core',
+ css: {
+ 'shape': 'rectangle',
+ 'active-bg-size': 0,
+ 'selection-box-color': 'rgb(0, 159, 219)',
+ 'selection-box-opacity': 0.2,
+ 'selection-box-border-color': '#009fdb',
+ 'selection-box-border-width': 1
+
+ }
+ },
+ {
+ selector: 'node',
+ css: {
+ 'font-family': 'OpenSans-Regular,sans-serif',
+
+ 'font-size': 14,
+ 'events': 'yes',
+ 'text-events': 'yes',
+ 'text-border-width': 15,
+ 'text-border-color': GraphColors.NODE_UCPE,
+ 'text-margin-y': 5
+ }
+ },
+ {
+ selector: '.vf-node',
+ css: {
+ 'background-color': 'transparent',
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'background-image': 'data(img)',
+ 'width': GraphUIObjects.DEFAULT_RESOURCE_WIDTH,
+ 'height': GraphUIObjects.DEFAULT_RESOURCE_WIDTH,
+ 'background-opacity': 0,
+ "background-width": GraphUIObjects.DEFAULT_RESOURCE_WIDTH,
+ "background-height": GraphUIObjects.DEFAULT_RESOURCE_WIDTH,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-fit': 'cover',
+ 'background-clip': 'node',
+ 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+
+ {
+ selector: '.service-node',
+ css: {
+ 'background-color': 'transparent',
+ 'label': 'data(displayName)',
+ 'events': 'yes',
+ 'text-events': 'yes',
+ 'background-image': 'data(img)',
+ 'width': 64,
+ 'height': 64,
+ "border-width": 0,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-opacity': 0,
+ 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+ {
+ selector: '.cp-node',
+ css: {
+ 'background-color': 'rgb(255,255,255)',
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'background-image': 'data(img)',
+ 'background-width': GraphUIObjects.SMALL_RESOURCE_WIDTH,
+ 'background-height': GraphUIObjects.SMALL_RESOURCE_WIDTH,
+ 'width': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE,
+ 'height': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE/2,
+ 'background-position-x': GraphUIObjects.HANDLE_SIZE / 2,
+ 'background-position-y': GraphUIObjects.HANDLE_SIZE / 2,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-opacity': 0,
+ 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+ {
+ selector: '.vl-node',
+ css: {
+ 'background-color': 'rgb(255,255,255)',
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'background-image': 'data(img)',
+ 'background-width': GraphUIObjects.SMALL_RESOURCE_WIDTH,
+ 'background-height': GraphUIObjects.SMALL_RESOURCE_WIDTH,
+ 'background-position-x': GraphUIObjects.HANDLE_SIZE / 2,
+ 'background-position-y': GraphUIObjects.HANDLE_SIZE / 2,
+ 'width': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE,
+ 'height': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE / 2,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-opacity': 0,
+ 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+ {
+ selector: '.ucpe-cp',
+ css: {
+ 'background-color': GraphColors.NODE_UCPE_CP,
+ 'background-width': 15,
+ 'background-height': 15,
+ 'width': 15,
+ 'height': 15,
+ 'text-halign': 'center',
+ 'overlay-opacity': 0,
+ 'label': 'data(displayName)',
+ 'text-valign': 'data(textPosition)',
+ 'text-margin-y': (ele:Cy.Collection) => {
+ return (ele.data('textPosition') == 'top') ? -5 : 5;
+ },
+ 'font-size': 12
+ }
+ },
+ {
+ selector: '.ucpe-node',
+ css: {
+ 'background-fit': 'cover',
+ 'padding-bottom': 0,
+ 'padding-top': 0
+ }
+ },
+ {
+ selector: '.simple-link',
+ css: {
+ 'width': 1,
+ 'line-color': GraphColors.BASE_LINK,
+ 'target-arrow-color': '#3b7b9b',
+ 'target-arrow-shape': 'triangle',
+ 'curve-style': 'bezier',
+ 'control-point-step-size': 30
+ }
+ },
+ {
+ selector: '.vl-link',
+ css: {
+ 'width': 3,
+ 'line-color': GraphColors.VL_LINK,
+ 'curve-style': 'bezier',
+ 'control-point-step-size': 30
+ }
+ },
+ {
+ selector: '.vl-link-1',
+ css: {
+ 'width': 3,
+ 'line-color': GraphColors.ACTIVE_LINK,
+ 'curve-style': 'unbundled-bezier',
+ 'target-arrow-color': '#3b7b9b',
+ 'target-arrow-shape': 'triangle',
+ 'control-point-step-size': 30
+ }
+ },
+ {
+ selector: '.ucpe-host-link',
+ css: {
+ 'width': 0
+ }
+ },
+ {
+ selector: '.not-certified-link',
+ css: {
+ 'width': 1,
+ 'line-color': GraphColors.NOT_CERTIFIED_LINK,
+ 'curve-style': 'bezier',
+ 'control-point-step-size': 30,
+ 'line-style': 'dashed',
+ 'target-arrow-color': '#3b7b9b',
+ 'target-arrow-shape': 'triangle'
+
+ }
+ },
+
+ {
+ selector: '.service-path-link',
+ css: {
+ 'width': 2,
+ 'line-color': GraphColors.SERVICE_PATH_LINK,
+ 'target-arrow-color': GraphColors.SERVICE_PATH_LINK,
+ 'target-arrow-shape': 'triangle',
+ 'curve-style': 'bezier',
+ 'control-point-step-size': 30
+ }
+ },
+ {
+ selector: '.not-certified',
+ css: {
+ 'shape': 'rectangle',
+ 'background-image': (ele:Cy.Collection) => {
+ // return ele.data().setUncertifiedImageBgStyle(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE);//Change name to setUncertifiedImageBgStyle??
+ return ele.data().initUncertifiedImage(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE);
+ },
+ 'border-width': 0
+ }
+ },
+ {
+ selector: '.dependent',
+ css: {
+ 'shape': 'rectangle',
+ 'background-image': (ele:Cy.Collection) => {
+ return ele.data().initDependentImage(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE)
+ },
+ 'border-width': 0
+ }
+ },
+ {
+ selector: '.dependent.not-certified',
+ css: {
+ 'shape': 'rectangle',
+ 'background-image': (ele:Cy.Collection) => {
+ return ele.data().initUncertifiedDependentImage(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE)
+ },
+ 'border-width': 0
+ }
+ },
+ {
+ selector: 'node:selected',
+ css: {
+ "border-width": 2,
+ "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR,
+ 'shape': 'rectangle'
+ }
+ },
+ {
+ selector: 'edge:selected',
+ css: {
+ 'line-color': GraphColors.ACTIVE_LINK
+
+ }
+ },
+ {
+ selector: 'edge:active',
+ css: {
+ 'overlay-opacity': 0
+ }
+ }, {
+ selector: '.configuration-node',
+ css: {
+ 'background-color': 'rgb(255,255,255)',
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'background-image': 'data(img)',
+ 'background-width': GraphUIObjects.SMALL_RESOURCE_WIDTH,
+ 'background-height': GraphUIObjects.SMALL_RESOURCE_WIDTH,
+ 'background-position-x': GraphUIObjects.HANDLE_SIZE / 2,
+ 'background-position-y': GraphUIObjects.HANDLE_SIZE / 2,
+ 'width': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE,
+ 'height': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE/2,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-opacity': 0,
+ 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+ {
+ selector: '.archived',
+ css: {
+ 'shape': 'rectangle',
+ 'background-image': (ele:Cy.Collection) => {
+ return ele.data().setArchivedImageBgStyle(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE); //Change name to setArchivedImageBgStyle ??
+ },
+ "border-width": 0
+ }
+ }
+ ]
+ }
+
+ public static getAddEdgeHandle = () => {
+ return {
+
+ single: false,
+ type: CanvasHandleTypes.ADD_EDGE,
+ imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_PLUS_ICON,
+ lineColor: '#27a337',
+ lineWidth: 2,
+ lineStyle: 'dashed'
+
+ }
+ }
+
+ public static getTagHandle = () => {
+ return {
+ single: false,
+ type: CanvasHandleTypes.TAG_AVAILABLE,
+ imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_TAG_ICON,
+ }
+ }
+
+ public static getTaggedPolicyHandle = () => {
+ return {
+ single: false,
+ type: CanvasHandleTypes.TAGGED_POLICY,
+ imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_POLICY_TAGGED_ICON,
+ }
+ }
+
+ public static getTaggedGroupHandle = () => {
+ return {
+ single: false,
+ type: CanvasHandleTypes.TAGGED_GROUP,
+ imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_GROUP_TAGGED_ICON,
+ }
+ }
+
+ public static getGraphDisplayName(name:string):string {
+ let context = document.createElement("canvas").getContext("2d");
+ context.font = "13px Arial";
+
+ if (67 < context.measureText(name).width) {
+ let newLen = name.length - 3;
+ let newName = name.substring(0, newLen);
+
+ while (59 < (context.measureText(newName).width)) {
+ newName = newName.substring(0, (--newLen));
+ }
+ return newName + '...';
+ }
+ return name;
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/style/module-node-style.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/module-node-style.ts
new file mode 100644
index 0000000000..bf71e1c868
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/module-node-style.ts
@@ -0,0 +1,103 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+import {GraphColors} from "app/utils";
+export class ModulesNodesStyle {
+
+ public static getModuleGraphStyle = ():Array<Cy.Stylesheet> => {
+
+ return [
+ {
+ selector: '.cy-expand-collapse-collapsed-node',
+ css: {
+ 'background-image': 'data(img)',
+ 'width': 34,
+ 'height': 32,
+ 'background-opacity': 0,
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'events': 'yes',
+ 'text-events': 'yes',
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'text-margin-y': 5,
+ 'border-opacity': 0
+ }
+ },
+ {
+ selector: '.module-node',
+ css: {
+ 'background-color': 'transparent',
+ 'background-opacity': 0,
+ "border-width": 2,
+ "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR,
+ 'border-style': 'dashed',
+ 'label': 'data(displayName)',
+ 'events': 'yes',
+ 'text-events': 'yes',
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'text-margin-y': 8
+ }
+ },
+ {
+ selector: 'node:selected',
+ css: {
+ "border-opacity": 0
+ }
+ },
+ {
+ selector: '.simple-link:selected',
+ css: {
+ 'line-color': GraphColors.BASE_LINK,
+ }
+ },
+ {
+ selector: '.vl-link:selected',
+ css: {
+ 'line-color': GraphColors.VL_LINK,
+ }
+ },
+ {
+ selector: '.cy-expand-collapse-collapsed-node:selected',
+ css: {
+ "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR,
+ 'border-opacity': 1,
+ 'border-style': 'solid',
+ 'border-width': 2
+ }
+ },
+ {
+ selector: '.module-node:selected',
+ css: {
+ "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR,
+ 'border-opacity': 1
+ }
+ },
+ {
+ selector: '.dummy-node',
+ css: {
+ 'width': 20,
+ 'height': 20
+ }
+ },
+ ]
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.html
new file mode 100644
index 0000000000..5a0ca3e43f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.html
@@ -0,0 +1,57 @@
+<div class="sdc-composition-graph-wrapper {{zoneTagMode}}"
+ [ngClass]="{'with-sidebar': withSidebar$ | async, 'view-only':isViewOnly$ | async}">
+</div>
+
+<div class="sdc-composition-menu" [ngClass]="{'with-sidebar': withSidebar$ | async}">
+
+ <service-path-selector
+ *ngIf="topologyTemplate.isService() && compositionService.forwardingPaths"
+ [drawPath]="drawPathOnCy"
+ [deletePaths]="deletePathsOnCy"
+ [selectedPathId]="selectedPathId">
+ </service-path-selector>
+
+ <canvas-search *ngIf="componentInstanceNames" class="composition-search"
+ [placeholder]="'Type to search'"
+ [data]="componentInstanceNames"
+ (searchChanged)="getAutoCompleteValues($event)"
+ (searchButtonClicked)="highlightSearchMatches($event)">
+ </canvas-search>
+
+ <!--<service-path class="zoom-icons"-->
+ <!--*ngIf="!(isViewOnly$ | async) && topologyTemplate.isService()"-->
+ <!--[service]="topologyTemplate"-->
+ <!--[onCreate]="createOrUpdateServicePath">-->
+ <!--</service-path>-->
+
+ <svg-icon *ngIf="!(isViewOnly$ | async) && topologyTemplate.isService()" class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'"
+ [backgroundColor]="'silver'" [name]="'browse'" [clickable]="true" [testId]="'pathsMenuBtn'"
+ (click)="openServicePathMenu($event)"></svg-icon>
+ <svg-icon class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'"
+ [backgroundColor]="'silver'" [name]="'expand-o'" [clickable]="true"
+ (click)="zoomAllWithoutSidebar()"></svg-icon>
+ <svg-icon class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'"
+ [backgroundColor]="'silver'" [name]="'plus'" [clickable]="true"
+ (click)="zoom(true)"></svg-icon>
+ <svg-icon class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'"
+ [backgroundColor]="'silver'" [name]="'minus'" [clickable]="true"
+ (click)="zoom(false)"></svg-icon>
+</div>
+
+<div class="sdc-canvas-zones__wrapper {{zoneTagMode}}" [ngClass]="{'with-sidebar': withSidebar$ | async}">
+ <zone-container *ngFor="let zone of zones" [title]="zone.title" [type]="zone.type" [count]="zone.instances.length"
+ [visible]="zone.visible" [minimized]="zone.minimized" (minimize)="zoneMinimizeToggle(zone.type)"
+ (backgroundClick)="zoneBackgroundClicked()">
+ <zone-instance *ngFor="let instance of zone.instances" [hidden]="instance.hidden"
+ [zoneInstance]="instance" [defaultIconText]="zone.defaultIconText"
+ [isActive]="activeZoneInstance == instance"
+ [activeInstanceMode]="activeZoneInstance && activeZoneInstance.mode"
+ [isViewOnly]="isViewOnly$ | async"
+ [forceSave]="instance.forceSave"
+ (modeChange)="zoneInstanceModeChanged($event.newMode, $event.instance, zone.type)"
+ (tagHandleClick)="zoneInstanceTagged($event)"
+ (assignmentSaveStart)="zoneAssignmentSaveStart()"
+ (assignmentSaveComplete)="zoneAssignmentSaveComplete($event)">
+ </zone-instance>
+ </zone-container>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.less
new file mode 100644
index 0000000000..b3e5ef3a0c
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.less
@@ -0,0 +1,93 @@
+:host(composition-graph) {
+ flex: 1;
+ padding-top: 53px;
+}
+
+.composition {
+ .custom-modal {
+ /* Hack solution to hide canvas tooltips under modals */
+ z-index: 20000 !important;
+ }
+}
+
+.sdc-composition-graph-wrapper {
+ height: 100%;
+ width: 100%;
+
+ &.with-sidebar {
+ width: calc(~'100% - 300px');
+ }
+}
+
+.view-only {
+ background-color: rgb(248, 248, 248);
+}
+
+.sdc-canvas-zones__wrapper {
+ position: absolute;
+ bottom: 10px;
+ right: 12px;
+ display: flex;
+ transition: right 0.2s;
+
+ &.with-sidebar {
+ right: 310px;
+ }
+
+ ng2-zone-container {
+ display: flex;
+ margin-left: 10px;
+ }
+}
+
+.group-tagging {
+ cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/group_1.svg"), pointer;
+}
+
+.group-tagging-hover {
+ cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/group_2.svg"), pointer;
+}
+
+.policy-tagging {
+ cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/policy_1.svg"), pointer;
+}
+
+.policy-tagging-hover {
+ cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/policy_2.svg"), pointer;
+}
+
+//Canvas menu
+.sdc-composition-menu {
+ position: absolute;
+ right: 18px;
+ top: 53px;
+ transition: right 0.2s;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ margin-right: 10px;
+ pointer-events: none;
+
+ & > * {
+ pointer-events: all;
+ }
+
+ &.with-sidebar {
+ right: 320px;
+ }
+
+ .composition-search {
+ margin-top: 12px;
+ }
+
+ .zoom-icons {
+ border: solid 1px #d2d2d2;
+ border-radius: 2px;
+ box-shadow: 0px 2px 3.88px 0.12px rgba(0, 0, 0, 0.29);
+ margin-top: 10px;
+
+ /deep/ .svg-icon {
+ box-sizing: content-box;
+ }
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.spec.ts
new file mode 100644
index 0000000000..9a15ecba69
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.spec.ts
@@ -0,0 +1,354 @@
+import {NO_ERRORS_SCHEMA} from '@angular/core';
+import {async, ComponentFixture} from '@angular/core/testing';
+import {SdcUiServices} from 'onap-ui-angular';
+import 'rxjs/add/observable/of';
+import {ConfigureFn, configureTests} from '../../../../../jest/test-config.helper';
+import {CompositionGraphComponent} from "./composition-graph.component";
+import {WorkspaceService} from "../../workspace/workspace.service";
+import {ComponentInstance, GroupInstance, NodesFactory, ZoneInstance, ZoneInstanceMode} from "../../../../models";
+import {EventListenerService} from "../../../../services";
+import {
+ CompositionGraphGeneralUtils,
+ CompositionGraphNodesUtils,
+ CompositionGraphZoneUtils,
+ MatchCapabilitiesRequirementsUtils, ServicePathGraphUtils
+} from "./utils";
+import {CompositionGraphLinkUtils} from "./utils/composition-graph-links-utils";
+import {ConnectionWizardService} from "./connection-wizard/connection-wizard.service";
+import {CommonGraphUtils} from "./common/common-graph-utils";
+import {CompositionGraphPaletteUtils} from "./utils/composition-graph-palette-utils";
+import {TopologyTemplateService} from "../../../services/component-services/topology-template.service";
+import {ComponentInstanceServiceNg2} from "../../../services/component-instance-services/component-instance.service";
+import {CompositionService} from "../composition.service";
+import {ModalService} from '../../../services/modal.service';
+import {Store} from '@ngxs/store';
+import {PoliciesService} from '../../../services/policies.service';
+import {GroupsService} from '../../../services/groups.service';
+import {PolicyInstance} from "../../../../models/graph/zones/policy-instance";
+import {ZoneInstanceType} from "../../../../models/graph/zones/zone-instance";
+import {GRAPH_EVENTS} from "../../../../utils/constants";
+import * as cytoscape from "cytoscape";
+import {ComponentMetadata} from "../../../../models/component-metadata";
+import {Zone} from "../../../../models/graph/zones/zone";
+import {SelectedComponentType, SetSelectedComponentAction} from "../common/store/graph.actions";
+
+describe('composition graph component', () => {
+
+ let fixture: ComponentFixture<CompositionGraphComponent>;
+ let instance: CompositionGraphComponent;
+ let eventServiceMock: Partial<EventListenerService>;
+ let compositionGraphZoneUtils: Partial<CompositionGraphZoneUtils>;
+ let generalGraphUtils: Partial<CompositionGraphGeneralUtils>;
+ let workspaceServiceMock: Partial<WorkspaceService>;
+ let policyService: Partial<PoliciesService>;
+ let storeStub;
+ let compositionGraphLinkUtils: Partial<CompositionGraphLinkUtils>;
+ let nodesGraphUtils: Partial<CompositionGraphNodesUtils>;
+
+ let createPolicyInstance = () => {
+ let policy = new PolicyInstance();
+ policy.targets = {COMPONENT_INSTANCES: [], GROUPS: []};
+ return new ZoneInstance(policy, '', '');
+ }
+
+ beforeEach(
+ async(() => {
+
+ eventServiceMock = {
+ notifyObservers: jest.fn(),
+ unRegisterObserver: jest.fn()
+ }
+
+ compositionGraphZoneUtils = {
+ endCyTagMode: jest.fn(),
+ showZoneTagIndications: jest.fn(),
+ hideZoneTagIndications: jest.fn(),
+ hideGroupZoneIndications: jest.fn(),
+ showGroupZoneIndications: jest.fn(),
+ startCyTagMode: jest.fn()
+ }
+
+ workspaceServiceMock = {
+ metadata: <ComponentMetadata>{
+ uniqueId: 'service_unique_id',
+ componentType: 'SERVICE'
+ }
+ }
+
+ compositionGraphLinkUtils = {
+ handleLinkClick: jest.fn(),
+ getModifyLinkMenu: jest.fn()
+ }
+
+ storeStub = {
+ dispatch: jest.fn()
+ }
+ policyService = {
+ getSpecificPolicy: jest.fn()
+ }
+
+ generalGraphUtils = {
+ zoomGraphTo: jest.fn()
+ }
+
+ nodesGraphUtils = {
+ onNodesPositionChanged: jest.fn()
+ }
+
+ const configure: ConfigureFn = (testBed) => {
+ testBed.configureTestingModule({
+ declarations: [CompositionGraphComponent],
+ imports: [],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: NodesFactory, useValue: {}},
+ {provide: EventListenerService, useValue: eventServiceMock},
+ {provide: CompositionGraphZoneUtils, useValue: compositionGraphZoneUtils},
+ {provide: CompositionGraphGeneralUtils, useValue: generalGraphUtils},
+ {provide: CompositionGraphLinkUtils, useValue: compositionGraphLinkUtils},
+ {provide: CompositionGraphNodesUtils, useValue: nodesGraphUtils},
+ {provide: ConnectionWizardService, useValue: {}},
+ {provide: CommonGraphUtils, useValue: {}},
+ {provide: CompositionGraphPaletteUtils, useValue: {}},
+ {provide: TopologyTemplateService, useValue: {}},
+ {provide: ComponentInstanceServiceNg2, useValue: {}},
+ {provide: MatchCapabilitiesRequirementsUtils, useValue: {}},
+ {provide: CompositionService, useValue: {}},
+ {provide: SdcUiServices.LoaderService, useValue: {}},
+ {provide: WorkspaceService, useValue: workspaceServiceMock},
+ {provide: SdcUiServices.NotificationsService, useValue: {}},
+ {provide: SdcUiServices.simplePopupMenuService, useValue: {}},
+ {provide: ServicePathGraphUtils, useValue: {}},
+ {provide: ModalService, useValue: {}},
+ {provide: PoliciesService, useValue: policyService},
+ {provide: GroupsService, useValue: {}},
+ {provide: Store, useValue: storeStub},
+ ],
+ });
+ };
+
+ configureTests(configure).then((testBed) => {
+ fixture = testBed.createComponent(CompositionGraphComponent);
+ instance = fixture.componentInstance;
+ instance._cy = cytoscape({});
+ });
+ })
+ );
+
+ it('composition graph component should be defined', () => {
+ expect(fixture).toBeDefined();
+ });
+
+ describe('on zone instance mode changed', () => {
+ let newZoneInstance: ZoneInstance;
+
+ beforeEach(
+ async(() => {
+ newZoneInstance = createPolicyInstance();
+ instance.zoneTagMode = null;
+ instance.zones = [];
+ instance.zones[ZoneInstanceType.POLICY] = new Zone('Policies', 'P', ZoneInstanceType.POLICY);
+ instance.zones[ZoneInstanceType.GROUP] = new Zone('Groups', 'G', ZoneInstanceType.GROUP);
+ instance.activeZoneInstance = createPolicyInstance();
+ }))
+
+ it('zone instance in tag mode and we want to turn tag mode off', () => {
+ instance.zoneTagMode = 'some_zone_id';
+ instance.activeZoneInstance = newZoneInstance;
+ instance.zoneInstanceModeChanged(ZoneInstanceMode.NONE, newZoneInstance, ZoneInstanceType.POLICY);
+ expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_CANVAS_TAG_END, newZoneInstance);
+ expect(instance.activeZoneInstance.mode).toBe(ZoneInstanceMode.SELECTED)
+ })
+
+ it('we are not in tag mode and policy instance mode changed to NONE - group and zone tag indication need to be removed', () => {
+ instance.zoneInstanceModeChanged(ZoneInstanceMode.NONE, newZoneInstance, ZoneInstanceType.POLICY);
+ expect(instance.compositionGraphZoneUtils.hideZoneTagIndications).toHaveBeenCalledWith(instance._cy);
+ expect(instance.compositionGraphZoneUtils.hideGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances);
+ })
+
+ it('we are not in tag mode and active zone instance gets hover/none - we dont actually change mode', () => {
+ let newMode = ZoneInstanceMode.SELECTED;
+ instance.zoneInstanceModeChanged(newMode, newZoneInstance, ZoneInstanceType.POLICY);
+ expect(newZoneInstance.mode).toBe(newMode);
+ })
+
+ it('we are not in tag mode and zone instance mode changed to HOVER mode', () => {
+ instance.zoneInstanceModeChanged(ZoneInstanceMode.HOVER, newZoneInstance, ZoneInstanceType.POLICY);
+ expect(instance.compositionGraphZoneUtils.showZoneTagIndications).toHaveBeenCalledWith(instance._cy, newZoneInstance);
+ expect(instance.compositionGraphZoneUtils.showGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances, newZoneInstance);
+ expect(instance.eventListenerService.notifyObservers).not.toHaveBeenCalled();
+ })
+
+ it('we are not in tag mode and mode changed to SELECTED', () => {
+ instance.zoneInstanceModeChanged(ZoneInstanceMode.SELECTED, newZoneInstance, ZoneInstanceType.POLICY);
+ expect(instance.compositionGraphZoneUtils.showZoneTagIndications).toHaveBeenCalledWith(instance._cy, newZoneInstance);
+ expect(instance.compositionGraphZoneUtils.showGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances, newZoneInstance);
+ expect(instance.activeZoneInstance).toBe(newZoneInstance);
+ expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, newZoneInstance);
+ expect(instance.store.dispatch).toHaveBeenCalledWith(new SetSelectedComponentAction({
+ component: newZoneInstance.instanceData,
+ type: SelectedComponentType[ZoneInstanceType[newZoneInstance.type]]
+ }));
+ expect(instance.eventListenerService.notifyObservers).not.toHaveBeenCalledWith(GRAPH_EVENTS.ON_CANVAS_TAG_START, ZoneInstanceType.POLICY);
+ })
+
+
+ it('we are not in tag mode and and zone instance mode changed to TAG', () => {
+ instance.zoneInstanceModeChanged(ZoneInstanceMode.TAG, newZoneInstance, ZoneInstanceType.POLICY);
+ expect(instance.compositionGraphZoneUtils.showZoneTagIndications).toHaveBeenCalledWith(instance._cy, newZoneInstance);
+ expect(instance.compositionGraphZoneUtils.showGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances, newZoneInstance);
+ expect(instance.activeZoneInstance).toBe(newZoneInstance);
+ expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, newZoneInstance);
+ expect(instance.store.dispatch).toHaveBeenCalledWith(new SetSelectedComponentAction({
+ component: newZoneInstance.instanceData,
+ type: SelectedComponentType[ZoneInstanceType[newZoneInstance.type]]
+ }));
+ expect(instance.compositionGraphZoneUtils.startCyTagMode).toHaveBeenCalledWith(instance._cy);
+ expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_CANVAS_TAG_START, ZoneInstanceType.POLICY);
+ })
+ })
+
+ it('unset active zone instance', () => {
+ instance.activeZoneInstance = createPolicyInstance();
+ instance.unsetActiveZoneInstance();
+ expect(instance.activeZoneInstance).toBeNull();
+ expect(instance.zoneTagMode).toBeNull();
+ })
+
+ it('zone background clicked - we are not in tag mode and active zone instance exist', () => {
+ instance.activeZoneInstance = createPolicyInstance();
+ jest.spyOn(instance, 'unsetActiveZoneInstance');
+ jest.spyOn(instance, 'selectTopologyTemplate');
+ instance.zoneBackgroundClicked();
+ expect(instance.unsetActiveZoneInstance).toHaveBeenCalled();
+ expect(instance.selectTopologyTemplate).toHaveBeenCalled();
+ })
+
+ it('zone background clicked - we are not in tag mode and no active zone instance exist', () => {
+ jest.spyOn(instance, 'unsetActiveZoneInstance');
+ jest.spyOn(instance, 'selectTopologyTemplate');
+ instance.zoneBackgroundClicked();
+ expect(instance.unsetActiveZoneInstance).not.toHaveBeenCalled();
+ expect(instance.selectTopologyTemplate).not.toHaveBeenCalled();
+ })
+
+ it('on zoom in', () => {
+ jest.spyOn(instance, 'zoom');
+ instance.zoom(true);
+ expect(instance.generalGraphUtils.zoomGraphTo).toHaveBeenCalledWith(instance._cy, instance._cy.zoom() + .1);
+ })
+
+ it('on zoom out', () => {
+ jest.spyOn(instance, 'zoom');
+ instance.zoom(false);
+ expect(instance.generalGraphUtils.zoomGraphTo).toHaveBeenCalledWith(instance._cy, instance._cy.zoom() - .1);
+ })
+
+ describe('cytoscape tap end event have been called', () => {
+
+ it('canvas background was clicked while zone instance in tag mode, zone instance still selected in tag mode)', () => {
+ let event = <Cy.EventObject>{cyTarget: instance._cy};
+ instance.zoneTagMode = 'instance_in_tag'
+ instance.onTapEnd(event);
+ expect(instance.zoneTagMode).toBe('instance_in_tag');
+ })
+
+ it('canvas background was clicked and no zone instance selected, topology template is now selected', () => {
+ let event = <Cy.EventObject>{cyTarget: instance._cy};
+ jest.spyOn(instance, 'selectTopologyTemplate');
+ instance.onTapEnd(event);
+ expect(instance.selectTopologyTemplate).toHaveBeenCalled();
+ expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED);
+ })
+
+ it('canvas background was clicked and zone instance was selected, topology template is now selected and zone instance is unselected', () => {
+ let event = <Cy.EventObject>{cyTarget: instance._cy};
+ instance.activeZoneInstance = createPolicyInstance();
+ jest.spyOn(instance, 'selectTopologyTemplate');
+ jest.spyOn(instance, 'unsetActiveZoneInstance');
+ instance.onTapEnd(event);
+ expect(instance.selectTopologyTemplate).toHaveBeenCalled();
+ expect(instance.unsetActiveZoneInstance).toHaveBeenCalled();
+ })
+
+
+ it('canvas background was clicked and zone instance was selected, topology template is now selected and zone instance is unselected', () => {
+ let event = <Cy.EventObject>{cyTarget: instance._cy};
+ instance.activeZoneInstance = createPolicyInstance();
+ jest.spyOn(instance, 'selectTopologyTemplate');
+ jest.spyOn(instance, 'unsetActiveZoneInstance');
+ instance.onTapEnd(event);
+ expect(instance.selectTopologyTemplate).toHaveBeenCalled();
+ expect(instance.unsetActiveZoneInstance).toHaveBeenCalled();
+ })
+
+ it('on simple edge clicked, open link menu and handle link click', () => {
+ let event = <Cy.EventObject>{
+ cyTarget: [{
+ isEdge: jest.fn().mockReturnValue(true),
+ data: jest.fn().mockReturnValue({type: 'simple'})
+ }
+ }];
+ instance.openModifyLinkMenu = jest.fn();
+ instance.onTapEnd(event);
+ expect(instance.compositionGraphLinkUtils.handleLinkClick).toHaveBeenCalledWith(instance._cy, event);
+ expect(instance.openModifyLinkMenu).toHaveBeenCalled();
+ })
+
+ it('on service path edge clicked, no menu is opened', () => {
+ let event = <Cy.EventObject>{
+ cyTarget: [{
+ isEdge: jest.fn().mockReturnValue(true),
+ data: jest.fn().mockReturnValue({type: 'service-path-link'})
+ }]
+ };
+ instance.openModifyLinkMenu = jest.fn();
+ instance.onTapEnd(event);
+ expect(instance.compositionGraphLinkUtils.handleLinkClick).toHaveBeenCalledWith(instance._cy, event);
+ expect(instance.openModifyLinkMenu).not.toHaveBeenCalled();
+ })
+
+ it('on drop after drag event (position has changed), call onNodesPositionChanged to update node position', () => {
+ let event = <Cy.EventObject>{
+ cyTarget: [{
+ isEdge: jest.fn().mockReturnValue(false),
+ position: jest.fn().mockReturnValue({x:2.11, y:2.44})
+ }]
+ };
+ instance.currentlyClickedNodePosition = <Cy.Position>{x:2.33, y:2.44};
+ instance.onTapEnd(event);
+ let nodesMoved: Cy.CollectionNodes = instance._cy.$(':grabbed');
+ expect(instance.nodesGraphUtils.onNodesPositionChanged).toHaveBeenCalledWith(instance._cy, instance.topologyTemplate, nodesMoved);
+
+ })
+
+ it('on node clicked (position not changed) while zone instance selected, unset active zone and call set selected instance', () => {
+ let event = <Cy.EventObject>{
+ cyTarget: [{
+ isEdge: jest.fn().mockReturnValue(false),
+ position: jest.fn().mockReturnValue({x:2.11, y:2.44}),
+ data: jest.fn().mockReturnValue({componentInstance: new ComponentInstance()})
+ }],
+ };
+ instance.currentlyClickedNodePosition = <Cy.Position>{x:2.11, y:2.44};
+ instance.activeZoneInstance = createPolicyInstance();
+ jest.spyOn(instance, 'unsetActiveZoneInstance');
+ jest.spyOn(instance, 'selectComponentInstance');
+ instance.onTapEnd(event);
+ expect(instance.unsetActiveZoneInstance).toHaveBeenCalled();
+ expect(instance.selectComponentInstance).toHaveBeenCalledWith(event.cyTarget[0].data().componentInstance);
+ })
+ })
+
+ it('initial view mode will turn off all cytoscape events', () => {
+ jest.spyOn(instance, 'isViewOnly').mockReturnValue(true);
+ jest.spyOn(instance._cy, 'off');
+ instance.initViewMode();
+ expect(instance._cy.off).toHaveBeenCalledWith('drag');
+ expect(instance._cy.off).toHaveBeenCalledWith('handlemouseout');
+ expect(instance._cy.off).toHaveBeenCalledWith('handlemouseover');
+ expect(instance._cy.off).toHaveBeenCalledWith('canvasredraw');
+ expect(instance._cy.off).toHaveBeenCalledWith('handletagclick');
+
+ })
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts
new file mode 100644
index 0000000000..69ca3faaf5
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts
@@ -0,0 +1,768 @@
+/**
+ * Created by ob0695 on 4/24/2018.
+ */
+import { AfterViewInit, Component, ElementRef, HostBinding, Input } from '@angular/core';
+import { Select, Store } from '@ngxs/store';
+import {
+ ButtonModel,
+ Component as TopologyTemplate,
+ ComponentInstance,
+ CompositionCiNodeBase,
+ ConnectRelationModel,
+ GroupInstance,
+ LeftPaletteComponent,
+ LinkMenu,
+ Match,
+ ModalModel,
+ NodesFactory,
+ Point,
+ PolicyInstance,
+ PropertyBEModel,
+ Relationship,
+ StepModel,
+ Zone,
+ ZoneInstance,
+ ZoneInstanceAssignmentType,
+ ZoneInstanceMode,
+ ZoneInstanceType
+} from 'app/models';
+import { ForwardingPath } from 'app/models/forwarding-path';
+import { CompositionCiServicePathLink } from 'app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link';
+import { UIZoneInstanceObject } from 'app/models/ui-models/ui-zone-instance-object';
+import { CompositionService } from 'app/ng2/pages/composition/composition.service';
+import { CommonGraphUtils } from 'app/ng2/pages/composition/graph/common/common-graph-utils';
+import { ComponentInstanceNodesStyle } from 'app/ng2/pages/composition/graph/common/style/component-instances-nodes-style';
+import { ConnectionPropertiesViewComponent } from 'app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component';
+import { ConnectionWizardHeaderComponent } from 'app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component';
+import { ConnectionWizardService } from 'app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service';
+import { FromNodeStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component';
+import { PropertiesStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component';
+import { ToNodeStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component';
+import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service';
+import { ComponentInstanceServiceNg2 } from 'app/ng2/services/component-instance-services/component-instance.service';
+import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service';
+import { ModalService } from 'app/ng2/services/modal.service';
+import { ComponentGenericResponse } from 'app/ng2/services/responses/component-generic-response';
+import { ServiceGenericResponse } from 'app/ng2/services/responses/service-generic-response';
+import { WorkspaceState } from 'app/ng2/store/states/workspace.state';
+import { EventListenerService } from 'app/services';
+import { ComponentInstanceFactory, EVENTS, SdcElementType } from 'app/utils';
+import { ComponentType, GRAPH_EVENTS, GraphColors, DEPENDENCY_EVENTS } from 'app/utils/constants';
+import * as _ from 'lodash';
+import { DndDropEvent } from 'ngx-drag-drop/ngx-drag-drop';
+import { SdcUiServices } from 'onap-ui-angular';
+import { NotificationSettings } from 'onap-ui-angular/dist/notifications/utilities/notification.config';
+import { menuItem } from 'onap-ui-angular/dist/simple-popup-menu/menu-data.interface';
+import { CytoscapeEdgeEditation } from '../../../../../third-party/cytoscape.js-edge-editation/CytoscapeEdgeEditation.js';
+import { SelectedComponentType, SetSelectedComponentAction } from '../common/store/graph.actions';
+import { GraphState } from '../common/store/graph.state';
+import {
+ CompositionGraphGeneralUtils,
+ CompositionGraphNodesUtils,
+ CompositionGraphZoneUtils,
+ MatchCapabilitiesRequirementsUtils
+} from './utils';
+import { CompositionGraphLinkUtils } from './utils/composition-graph-links-utils';
+import { CompositionGraphPaletteUtils } from './utils/composition-graph-palette-utils';
+import { ServicePathGraphUtils } from './utils/composition-graph-service-path-utils';
+
+declare const window: any;
+
+@Component({
+ selector: 'composition-graph',
+ templateUrl: './composition-graph.component.html',
+ styleUrls: ['./composition-graph.component.less']
+})
+
+export class CompositionGraphComponent implements AfterViewInit {
+
+ @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean;
+ @Select(GraphState.withSidebar) withSidebar$: boolean;
+ @Input() topologyTemplate: TopologyTemplate;
+ @HostBinding('attr.data-tests-id') dataTestId: string;
+ @Input() testId: string;
+
+ // tslint:disable:variable-name
+ private _cy: Cy.Instance;
+ private zoneTagMode: string;
+ private activeZoneInstance: ZoneInstance;
+ private zones: Zone[];
+ private currentlyClickedNodePosition: Cy.Position;
+ private dragElement: JQuery;
+ private dragComponent: ComponentInstance;
+ private componentInstanceNames: string[];
+ private topologyTemplateId: string;
+ private topologyTemplateType: string;
+
+ constructor(private elRef: ElementRef,
+ private nodesFactory: NodesFactory,
+ private eventListenerService: EventListenerService,
+ private compositionGraphZoneUtils: CompositionGraphZoneUtils,
+ private generalGraphUtils: CompositionGraphGeneralUtils,
+ private compositionGraphLinkUtils: CompositionGraphLinkUtils,
+ private nodesGraphUtils: CompositionGraphNodesUtils,
+ private connectionWizardService: ConnectionWizardService,
+ private commonGraphUtils: CommonGraphUtils,
+ private modalService: ModalService,
+ private compositionGraphPaletteUtils: CompositionGraphPaletteUtils,
+ private topologyTemplateService: TopologyTemplateService,
+ private componentInstanceService: ComponentInstanceServiceNg2,
+ private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils,
+ private store: Store,
+ private compositionService: CompositionService,
+ private loaderService: SdcUiServices.LoaderService,
+ private workspaceService: WorkspaceService,
+ private notificationService: SdcUiServices.NotificationsService,
+ private simplePopupMenuService: SdcUiServices.simplePopupMenuService,
+ private servicePathGraphUtils: ServicePathGraphUtils) {
+ }
+
+ ngOnInit() {
+ this.dataTestId = this.testId;
+ this.topologyTemplateId = this.workspaceService.metadata.uniqueId;
+ this.topologyTemplateType = this.workspaceService.metadata.componentType;
+
+ this.store.dispatch(new SetSelectedComponentAction({
+ component: this.topologyTemplate,
+ type: SelectedComponentType.TOPOLOGY_TEMPLATE
+ }));
+ this.eventListenerService.registerObserverCallback(EVENTS.ON_CHECKOUT, () => {
+ this.loadGraphData();
+ });
+ this.loadCompositionData();
+ }
+
+ ngAfterViewInit() {
+ this.loadGraph();
+ }
+
+ ngOnDestroy() {
+ this._cy.destroy();
+ _.forEach(GRAPH_EVENTS, (event) => {
+ this.eventListenerService.unRegisterObserver(event);
+ });
+ this.eventListenerService.unRegisterObserver(EVENTS.ON_CHECKOUT);
+ this.eventListenerService.unRegisterObserver(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE);
+ }
+
+ public isViewOnly = (): boolean => {
+ return this.store.selectSnapshot((state) => state.workspace.isViewOnly);
+ }
+
+ public zoom = (zoomIn: boolean): void => {
+ const currentZoom: number = this._cy.zoom();
+ if (zoomIn) {
+ this.generalGraphUtils.zoomGraphTo(this._cy, currentZoom + .1);
+ } else {
+ this.generalGraphUtils.zoomGraphTo(this._cy, currentZoom - .1);
+ }
+ }
+
+ public zoomAllWithoutSidebar = () => {
+ setTimeout(() => { // wait for sidebar changes to take effect before zooming
+ this.generalGraphUtils.zoomAll(this._cy);
+ });
+ }
+
+ public getAutoCompleteValues = (searchTerm: string) => {
+ if (searchTerm.length > 1) { // US requirement: only display search results after 2nd letter typed.
+ const nodes: Cy.CollectionNodes = this.nodesGraphUtils.getMatchingNodesByName(this._cy, searchTerm);
+ this.componentInstanceNames = _.map(nodes, (node) => node.data('name'));
+ } else {
+ this.componentInstanceNames = [];
+ }
+ }
+
+ public highlightSearchMatches = (searchTerm: string) => {
+ this.nodesGraphUtils.highlightMatchingNodesByName(this._cy, searchTerm);
+ const matchingNodes: Cy.CollectionNodes = this.nodesGraphUtils.getMatchingNodesByName(this._cy, searchTerm);
+ this.generalGraphUtils.zoomAll(this._cy, matchingNodes);
+ }
+
+ public onDrop = (dndEvent: DndDropEvent) => {
+ this.compositionGraphPaletteUtils.addNodeFromPalette(this._cy, dndEvent);
+ }
+
+ public openServicePathMenu = ($event): void => {
+
+ const menuConfig: menuItem[] = [];
+ if (!this.isViewOnly()) {
+ menuConfig.push({
+ text: 'Create Service Flow',
+ action: () => this.servicePathGraphUtils.onCreateServicePath()
+ });
+ }
+ menuConfig.push({
+ text: 'Service Flows List',
+ type: '',
+ action: () => this.servicePathGraphUtils.onListServicePath()
+ });
+ const popup = this.simplePopupMenuService.openBaseMenu(menuConfig, {
+ x: $event.x,
+ y: $event.y
+ });
+
+ }
+
+ public deletePathsOnCy = () => {
+ this.servicePathGraphUtils.deletePathsFromGraph(this._cy);
+ }
+
+ public drawPathOnCy = (data: ForwardingPath) => {
+ this.servicePathGraphUtils.drawPath(this._cy, data);
+ }
+
+ public onTapEnd = (event: Cy.EventObject) => {
+ if (this.zoneTagMode) {
+ return;
+ }
+ if (event.cyTarget === this._cy) { // On Background clicked
+ if (this._cy.$('node:selected').length === 0) { // if the background click but not dragged
+ if (this.activeZoneInstance) {
+ this.unsetActiveZoneInstance();
+ this.selectTopologyTemplate();
+ } else {
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED);
+ this.selectTopologyTemplate();
+ }
+
+ }
+ } else if (event.cyTarget[0].isEdge()) { // and Edge clicked
+ this.compositionGraphLinkUtils.handleLinkClick(this._cy, event);
+ if (event.cyTarget[0].data().type === CompositionCiServicePathLink.LINK_TYPE) {
+ return;
+ }
+ this.openModifyLinkMenu(this.compositionGraphLinkUtils.getModifyLinkMenu(event.cyTarget[0], event), event);
+ } else { // On Node clicked
+
+ this._cy.nodes(':grabbed').style({'overlay-opacity': 0});
+
+ const 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) {
+ const nodesMoved: Cy.CollectionNodes = this._cy.$(':grabbed');
+ this.nodesGraphUtils.onNodesPositionChanged(this._cy, this.topologyTemplate, nodesMoved);
+ } else {
+ if (this.activeZoneInstance) {
+ this.unsetActiveZoneInstance();
+ }
+ this.selectComponentInstance(event.cyTarget[0].data().componentInstance);
+ }
+ }
+ }
+
+ private registerCytoscapeGraphEvents() {
+
+ this._cy.on('addedgemouseup', (event, data) => {
+ const connectRelationModel: ConnectRelationModel = this.compositionGraphLinkUtils.onLinkDrawn(this._cy, data.source, data.target);
+ if (connectRelationModel != null) {
+ this.connectionWizardService.setRelationMenuDirectiveObj(connectRelationModel);
+ this.connectionWizardService.selectedMatch = null;
+
+ const steps: StepModel[] = [];
+ const fromNodeName: string = connectRelationModel.fromNode.componentInstance.name;
+ const toNodeName: string = connectRelationModel.toNode.componentInstance.name;
+ steps.push(new StepModel(fromNodeName, FromNodeStepComponent));
+ steps.push(new StepModel(toNodeName, ToNodeStepComponent));
+ steps.push(new StepModel('Properties', PropertiesStepComponent));
+ const wizardTitle = 'Connect: ' + fromNodeName + ' to ' + toNodeName;
+ const modalInstance = this.modalService.createMultiStepsWizard(wizardTitle, steps, this.createLinkFromMenu, ConnectionWizardHeaderComponent);
+ modalInstance.instance.open();
+ }
+ });
+
+ this._cy.on('tapstart', 'node', (event: Cy.EventObject) => {
+ this.currentlyClickedNodePosition = angular.copy(event.cyTarget[0].position()); // update node position on drag
+ });
+
+ this._cy.on('drag', 'node', (event: Cy.EventObject) => {
+ if (event.cyTarget.data().componentSubType !== SdcElementType.POLICY && event.cyTarget.data().componentSubType !== SdcElementType.GROUP) {
+ 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});
+ }
+ }
+ });
+
+ this._cy.on('handlemouseover', (event, payload) => {
+ // no need to add opacity while we are dragging and hovering othe nodes- or if opacity was already calculated for these nodes
+ if (payload.node.grabbed() || this._cy.scratch('_edge_editation_highlights') === true) {
+ return;
+ }
+
+ if (this.zoneTagMode) {
+ this.zoneTagMode = this.zones[this.activeZoneInstance.type].getHoverTagModeId();
+ return;
+ }
+
+ const nodesData = this.nodesGraphUtils.getAllNodesData(this._cy.nodes());
+ const nodesLinks = this.generalGraphUtils.getAllCompositionCiLinks(this._cy);
+ const instance = payload.node.data().componentInstance;
+ const filteredNodesData = this.matchCapabilitiesRequirementsUtils.findMatchingNodesToComponentInstance(instance, nodesData, nodesLinks);
+ this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, this._cy);
+ this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, this._cy, payload.node.data());
+
+ this._cy.scratch()._edge_editation_highlights = true;
+ });
+
+ this._cy.on('handlemouseout', () => {
+ if (this.zoneTagMode) {
+ this.zoneTagMode = this.zones[this.activeZoneInstance.type].getTagModeId();
+ return;
+ }
+ 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) => {
+ this.onTapEnd(event);
+ });
+
+ this._cy.on('boxselect', 'node', (event: Cy.EventObject) => {
+ this.unsetActiveZoneInstance();
+ this.selectComponentInstance(event.cyTarget.data().componentInstance);
+ });
+
+ this._cy.on('canvasredraw', (event: Cy.EventObject) => {
+ if (this.zoneTagMode) {
+ this.compositionGraphZoneUtils.showZoneTagIndications(this._cy, this.activeZoneInstance);
+ }
+ });
+
+ this._cy.on('handletagclick', (event: Cy.EventObject, eventData: any) => {
+ this.compositionGraphZoneUtils.handleTagClick(this._cy, this.activeZoneInstance, eventData.nodeId);
+ });
+ }
+
+ private initViewMode() {
+
+ if (this.isViewOnly()) {
+ // remove event listeners
+ this._cy.off('drag');
+ this._cy.off('handlemouseout');
+ this._cy.off('handlemouseover');
+ this._cy.off('canvasredraw');
+ this._cy.off('handletagclick');
+ this._cy.edges().unselectify();
+ }
+ }
+
+ private saveChangedCapabilityProperties = (): Promise<PropertyBEModel[]> => {
+ return new Promise<PropertyBEModel[]>((resolve) => {
+ const capabilityPropertiesBE: PropertyBEModel[] = this.connectionWizardService.changedCapabilityProperties.map((prop) => {
+ prop.value = prop.getJSONValue();
+ const propBE = new PropertyBEModel(prop);
+ propBE.parentUniqueId = this.connectionWizardService.selectedMatch.relationship.relation.capabilityOwnerId;
+ return propBE;
+ });
+ if (capabilityPropertiesBE.length > 0) {
+ // if there are capability properties to update, then first update capability properties and then resolve promise
+ this.componentInstanceService
+ .updateInstanceCapabilityProperties(
+ this.topologyTemplate,
+ this.connectionWizardService.selectedMatch.toNode,
+ this.connectionWizardService.selectedMatch.capability,
+ capabilityPropertiesBE
+ )
+ .subscribe((response) => {
+ console.log('Update resource instance capability properties response: ', response);
+ this.connectionWizardService.changedCapabilityProperties = [];
+ resolve(capabilityPropertiesBE);
+ });
+ } else {
+ // no capability properties to update, immediately resolve promise
+ resolve(capabilityPropertiesBE);
+ }
+ });
+ }
+
+ private loadCompositionData = () => {
+ this.loaderService.activate();
+ this.topologyTemplateService.getComponentCompositionData(this.topologyTemplateId, this.topologyTemplateType).subscribe((response: ComponentGenericResponse) => {
+ if (this.topologyTemplateType === ComponentType.SERVICE) {
+ this.compositionService.forwardingPaths = (response as ServiceGenericResponse).forwardingPaths;
+ }
+ this.compositionService.componentInstances = response.componentInstances;
+ this.compositionService.componentInstancesRelations = response.componentInstancesRelations;
+ this.compositionService.groupInstances = response.groupInstances;
+ this.compositionService.policies = response.policies;
+ this.loadGraphData();
+ this.loaderService.deactivate();
+ }, (error) => { this.loaderService.deactivate(); });
+ }
+
+ private loadGraph = () => {
+ const graphEl = this.elRef.nativeElement.querySelector('.sdc-composition-graph-wrapper');
+ this.initGraph(graphEl);
+ this.zones = this.compositionGraphZoneUtils.createCompositionZones();
+ this.registerCytoscapeGraphEvents();
+ this.registerCustomEvents();
+ this.initViewMode();
+ }
+
+ private initGraphNodes() {
+
+ setTimeout(() => {
+ const handles = new CytoscapeEdgeEditation();
+ handles.init(this._cy);
+ if (!this.isViewOnly()) { // Init nodes handle extension - enable dynamic links
+ handles.initNodeEvents();
+ handles.registerHandle(ComponentInstanceNodesStyle.getAddEdgeHandle());
+ }
+ handles.registerHandle(ComponentInstanceNodesStyle.getTagHandle());
+ handles.registerHandle(ComponentInstanceNodesStyle.getTaggedPolicyHandle());
+ handles.registerHandle(ComponentInstanceNodesStyle.getTaggedGroupHandle());
+ }, 0);
+
+ _.each(this.compositionService.componentInstances, (instance) => {
+ const compositionGraphNode: CompositionCiNodeBase = this.nodesFactory.createNode(instance);
+ this.commonGraphUtils.addComponentInstanceNodeToGraph(this._cy, compositionGraphNode);
+ });
+
+ }
+
+ private loadGraphData = () => {
+ this.initGraphNodes();
+ this.compositionGraphLinkUtils.initGraphLinks(this._cy, this.compositionService.componentInstancesRelations);
+ this.compositionGraphZoneUtils.initZoneInstances(this.zones);
+ setTimeout(() => { // Need setTimeout so that angular canvas changes will take effect before resize & center
+ this.generalGraphUtils.zoomAllWithMax(this._cy, 1);
+ });
+ this.componentInstanceNames = _.map(this._cy.nodes(), (node) => node.data('name'));
+ }
+
+ private initGraph(graphEl: JQuery) {
+
+ this._cy = cytoscape({
+ container: graphEl,
+ style: ComponentInstanceNodesStyle.getCompositionGraphStyle(),
+ zoomingEnabled: true,
+ maxZoom: 1.2,
+ minZoom: .1,
+ userZoomingEnabled: false,
+ userPanningEnabled: true,
+ selectionType: 'single',
+ boxSelectionEnabled: true,
+ autolock: this.isViewOnly(),
+ autoungrabify: this.isViewOnly()
+ });
+
+ // Testing Bridge that allows Cypress tests to select a component on canvas not via DOM
+ if (window.Cypress) {
+ window.testBridge = this.createCanvasTestBridge();
+ }
+ }
+
+ private createCanvasTestBridge(): any {
+ return {
+ selectComponentInstance: (componentName: string) => {
+ const matchingNodesByName = this.nodesGraphUtils.getMatchingNodesByName(this._cy, componentName);
+ const component = new ComponentInstance(matchingNodesByName.first().data().componentInstance);
+ this.selectComponentInstance(component);
+ }
+ };
+ }
+
+ // -------------------------------------------- ZONES---------------------------------------------------------//
+ private zoneMinimizeToggle = (zoneType: ZoneInstanceType): void => {
+ this.zones[zoneType].minimized = !this.zones[zoneType].minimized;
+ }
+
+ private zoneInstanceModeChanged = (newMode: ZoneInstanceMode, instance: ZoneInstance, zoneId: ZoneInstanceType): void => {
+ if (this.zoneTagMode) { // we're in tag mode.
+ if (instance === this.activeZoneInstance && newMode === ZoneInstanceMode.NONE) { // we want to turn tag mode off.
+ this.zoneTagMode = null;
+ this.activeZoneInstance.mode = ZoneInstanceMode.SELECTED;
+ this.compositionGraphZoneUtils.endCyTagMode(this._cy);
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CANVAS_TAG_END, instance);
+
+ }
+ } else {
+ // when active zone instance gets hover/none, don't actually change mode, just show/hide indications
+ if (instance !== this.activeZoneInstance || (instance === this.activeZoneInstance && newMode > ZoneInstanceMode.HOVER)) {
+ instance.mode = newMode;
+ }
+
+ if (newMode === ZoneInstanceMode.NONE) {
+ this.compositionGraphZoneUtils.hideZoneTagIndications(this._cy);
+ if (this.zones[ZoneInstanceType.GROUP]) {
+ this.compositionGraphZoneUtils.hideGroupZoneIndications(this.zones[ZoneInstanceType.GROUP].instances);
+ }
+ }
+ if (newMode >= ZoneInstanceMode.HOVER) {
+ this.compositionGraphZoneUtils.showZoneTagIndications(this._cy, instance);
+ if (instance.type === ZoneInstanceType.POLICY && this.zones[ZoneInstanceType.GROUP]) {
+ this.compositionGraphZoneUtils.showGroupZoneIndications(this.zones[ZoneInstanceType.GROUP].instances, instance);
+ }
+ }
+ if (newMode >= ZoneInstanceMode.SELECTED) {
+ this._cy.$('node:selected').unselect();
+ if (this.activeZoneInstance && this.activeZoneInstance !== instance && newMode >= ZoneInstanceMode.SELECTED) {
+ this.activeZoneInstance.mode = ZoneInstanceMode.NONE;
+ }
+ this.activeZoneInstance = instance;
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, instance);
+ this.store.dispatch(new SetSelectedComponentAction({
+ component: instance.instanceData,
+ type: SelectedComponentType[ZoneInstanceType[instance.type]]
+ }));
+ }
+ if (newMode === ZoneInstanceMode.TAG) {
+ this.compositionGraphZoneUtils.startCyTagMode(this._cy);
+ this.zoneTagMode = this.zones[zoneId].getTagModeId();
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CANVAS_TAG_START, zoneId);
+ }
+ }
+ }
+
+ private zoneInstanceTagged = (taggedInstance: ZoneInstance) => {
+ this.activeZoneInstance.addOrRemoveAssignment(taggedInstance.instanceData.uniqueId, ZoneInstanceAssignmentType.GROUPS);
+ const newHandle: string = this.compositionGraphZoneUtils.getCorrectHandleForNode(taggedInstance.instanceData.uniqueId, this.activeZoneInstance);
+ taggedInstance.showHandle(newHandle);
+ }
+
+ private unsetActiveZoneInstance = (): void => {
+ if (this.activeZoneInstance) {
+ this.activeZoneInstance.mode = ZoneInstanceMode.NONE;
+ this.activeZoneInstance = null;
+ this.zoneTagMode = null;
+ }
+ }
+
+ private selectComponentInstance = (componentInstance: ComponentInstance) => {
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_NODE_SELECTED, componentInstance);
+ this.store.dispatch(new SetSelectedComponentAction({
+ component: componentInstance,
+ type: SelectedComponentType.COMPONENT_INSTANCE
+ }));
+ }
+
+ private selectTopologyTemplate = () => {
+ this.store.dispatch(new SetSelectedComponentAction({
+ component: this.topologyTemplate,
+ type: SelectedComponentType.TOPOLOGY_TEMPLATE
+ }));
+ }
+
+ private zoneBackgroundClicked = (): void => {
+ if (!this.zoneTagMode && this.activeZoneInstance) {
+ this.unsetActiveZoneInstance();
+ this.selectTopologyTemplate();
+ }
+ }
+
+ private zoneAssignmentSaveStart = () => {
+ this.loaderService.activate();
+ }
+
+ private zoneAssignmentSaveComplete = (success: boolean) => {
+ this.loaderService.deactivate();
+ if (!success) {
+ this.notificationService.push(new NotificationSettings('error', 'Update Failed', 'Error'));
+ }
+ }
+
+ private deleteZoneInstance = (deletedInstance: UIZoneInstanceObject) => {
+ if (deletedInstance.type === ZoneInstanceType.POLICY) {
+ this.compositionService.policies = this.compositionService.policies.filter((policy) => policy.uniqueId !== deletedInstance.uniqueId);
+ } else if (deletedInstance.type === ZoneInstanceType.GROUP) {
+ this.compositionService.groupInstances = this.compositionService.groupInstances.filter((group) => group.uniqueId !== deletedInstance.uniqueId);
+ }
+ // remove it from zones
+ this.zones[deletedInstance.type].removeInstance(deletedInstance.uniqueId);
+ if (deletedInstance.type === ZoneInstanceType.GROUP && !_.isEmpty(this.zones[ZoneInstanceType.POLICY])) {
+ this.compositionGraphZoneUtils.updateTargetsOrMembersOnCanvasDelete(deletedInstance.uniqueId, [this.zones[ZoneInstanceType.POLICY]], ZoneInstanceAssignmentType.GROUPS);
+ }
+ this.selectTopologyTemplate();
+ }
+ // -------------------------------------------------------------------------------------------------------------//
+
+ private registerCustomEvents() {
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, (groupInstance: GroupInstance) => {
+ this.compositionGraphZoneUtils.findAndUpdateZoneInstanceData(this.zones, groupInstance);
+ this.notificationService.push(new NotificationSettings('success', 'Group Updated', 'Success'));
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, (policyInstance: PolicyInstance) => {
+ this.compositionGraphZoneUtils.findAndUpdateZoneInstanceData(this.zones, policyInstance);
+ this.notificationService.push(new NotificationSettings('success', 'Policy Updated', 'Success'));
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, (leftPaletteComponent: LeftPaletteComponent) => {
+ this.compositionGraphPaletteUtils.onComponentHoverIn(leftPaletteComponent, this._cy);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_ADD_ZONE_INSTANCE_FROM_PALETTE,
+ (component: TopologyTemplate, paletteComponent: LeftPaletteComponent, startPosition: Point) => {
+
+ const zoneType: ZoneInstanceType = this.compositionGraphZoneUtils.getZoneTypeForPaletteComponent(paletteComponent.categoryType);
+ this.compositionGraphZoneUtils.showZone(this.zones[zoneType]);
+
+ this.loaderService.activate();
+ this.compositionGraphZoneUtils.createZoneInstanceFromLeftPalette(zoneType, paletteComponent.type).subscribe((zoneInstance: ZoneInstance) => {
+ this.loaderService.deactivate();
+ this.compositionGraphZoneUtils.addInstanceToZone(this.zones[zoneInstance.type], zoneInstance, true);
+ this.compositionGraphZoneUtils.createPaletteToZoneAnimation(startPosition, zoneType, zoneInstance);
+ }, (error) => {
+ this.loaderService.deactivate();
+ });
+ });
+
+ 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 = ComponentInstanceFactory.createComponentInstanceFromComponent(dragComponent);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_ACTION, (position: Point) => {
+ const draggedElement = document.getElementById('draggable_element');
+ draggedElement.className = this.compositionGraphPaletteUtils.isDragValid(this._cy, position) ? 'valid-drag' : 'invalid-drag';
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DROP, (event: DndDropEvent) => {
+ this.onDrop(event);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_NAME_CHANGED, (component: ComponentInstance) => {
+ const 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, (componentInstanceId: string) => {
+ const nodeToDelete = this._cy.getElementById(componentInstanceId);
+ this.nodesGraphUtils.deleteNode(this._cy, this.topologyTemplate, nodeToDelete);
+ this.selectTopologyTemplate();
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE, (deletedInstance: UIZoneInstanceObject) => {
+ this.deleteZoneInstance(deletedInstance);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, (componentInstanceId: string) => {
+ if (!_.isEmpty(this.zones)) {
+ this.compositionGraphZoneUtils.updateTargetsOrMembersOnCanvasDelete(componentInstanceId, this.zones, ZoneInstanceAssignmentType.COMPONENT_INSTANCES);
+ }
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_EDGE, (releaseLoading: boolean, linksToDelete: Cy.CollectionEdges) => {
+ this.compositionGraphLinkUtils.deleteLink(this._cy, this.topologyTemplate, releaseLoading, linksToDelete);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_VERSION_CHANGED, (component: ComponentInstance) => {
+ // Remove everything from graph and reload it all
+ this._cy.elements().remove();
+ this.loadCompositionData();
+ setTimeout(() => { this._cy.getElementById(component.uniqueId).select(); }, 1000);
+ this.selectComponentInstance(component);
+ });
+ this.eventListenerService.registerObserverCallback(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE, (ischecked: boolean) => {
+ if (ischecked) {
+ this._cy.$('node:selected').addClass('dependent');
+ } else {
+ // 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.
+ this._cy.$('node:selected').removeClass('dependent');
+ this._cy.$('node:selected').style({'background-image': this._cy.$('node:selected').data('originalImg')});
+ }
+ });
+ }
+ private createLinkFromMenu = (): void => {
+ this.saveChangedCapabilityProperties().then(() => {
+ this.compositionGraphLinkUtils.createLinkFromMenu(this._cy, this.connectionWizardService.selectedMatch);
+ });
+ }
+
+ private deleteRelation = (link: Cy.CollectionEdges) => {
+ // 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, this.topologyTemplate, this._cy.$('node:selected'));
+ } else {
+ this.compositionGraphLinkUtils.deleteLink(this._cy, this.topologyTemplate, true, link);
+ }
+ }
+
+ private viewRelation = (link: Cy.CollectionEdges) => {
+
+ const linkData = link.data();
+ const sourceNode: CompositionCiNodeBase = link.source().data();
+ const targetNode: CompositionCiNodeBase = link.target().data();
+ const relationship: Relationship = linkData.relation.relationships[0];
+
+ this.compositionGraphLinkUtils.getRelationRequirementCapability(relationship, sourceNode.componentInstance, targetNode.componentInstance).then((objReqCap) => {
+ const capability = objReqCap.capability;
+ const requirement = objReqCap.requirement;
+
+ this.connectionWizardService.connectRelationModel = new ConnectRelationModel(sourceNode, targetNode, []);
+ this.connectionWizardService.selectedMatch = new Match(requirement, capability, true, linkData.source, linkData.target);
+ this.connectionWizardService.selectedMatch.relationship = relationship;
+
+ const title = `Connection Properties`;
+ const saveButton: ButtonModel = new ButtonModel('Save', 'blue', () => {
+ this.saveChangedCapabilityProperties().then(() => {
+ this.modalService.closeCurrentModal();
+ });
+ });
+ const cancelButton: ButtonModel = new ButtonModel('Cancel', 'white', () => {
+ this.modalService.closeCurrentModal();
+ });
+ const modal = new ModalModel('xl', title, '', [saveButton, cancelButton]);
+ const modalInstance = this.modalService.createCustomModal(modal);
+ this.modalService.addDynamicContentToModal(modalInstance, ConnectionPropertiesViewComponent);
+ modalInstance.instance.open();
+
+ new Promise((resolve) => {
+ if (!this.connectionWizardService.selectedMatch.capability.properties) {
+ this.componentInstanceService.getInstanceCapabilityProperties(this.topologyTemplateType, this.topologyTemplateId, linkData.target, capability)
+ .subscribe(() => {
+ resolve();
+ }, () => { /* do nothing */ });
+ } else {
+ resolve();
+ }
+ }).then(() => {
+ this.modalService.addDynamicContentToModal(modalInstance, ConnectionPropertiesViewComponent);
+ });
+ }, () => { /* do nothing */ });
+ }
+
+ private openModifyLinkMenu = (linkMenuObject: LinkMenu, $event) => {
+
+ const menuConfig: menuItem[] = [{
+ text: 'View',
+ iconName: 'eye-o',
+ iconType: 'common',
+ iconMode: 'secondary',
+ iconSize: 'small',
+ type: '',
+ action: () => this.viewRelation(linkMenuObject.link as Cy.CollectionEdges)
+ }];
+
+ if (!this.isViewOnly()) {
+ menuConfig.push({
+ text: 'Delete',
+ iconName: 'trash-o',
+ iconType: 'common',
+ iconMode: 'secondary',
+ iconSize: 'small',
+ type: '',
+ action: () => this.deleteRelation(linkMenuObject.link as Cy.CollectionEdges)
+ });
+ }
+ this.simplePopupMenuService.openBaseMenu(menuConfig, {
+ x: $event.originalEvent.x,
+ y: $event.originalEvent.y
+ });
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.module.ts
new file mode 100644
index 0000000000..e58d160c4d
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.module.ts
@@ -0,0 +1,55 @@
+import {NgModule} from "@angular/core";
+import {CommonModule} from "@angular/common";
+import {CompositionGraphComponent} from "./composition-graph.component";
+import {ZoneModules} from "./canvas-zone/zones-module";
+import {CompositionGraphZoneUtils} from "./utils/composition-graph-zone-utils";
+import {CompositionGraphGeneralUtils} from "./utils/composition-graph-general-utils";
+import {CommonGraphUtils} from "./common/common-graph-utils";
+import {LinksFactory} from "app/models/graph/graph-links/links-factory";
+import {NodesFactory} from "app/models/graph/nodes/nodes-factory";
+import {ImageCreatorService} from "./common/image-creator.service";
+import {MatchCapabilitiesRequirementsUtils} from "./utils/match-capability-requierment-utils";
+import {CompositionGraphNodesUtils} from "./utils/composition-graph-nodes-utils";
+import {ConnectionWizardService} from "app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service";
+import {CompositionGraphPaletteUtils} from "./utils/composition-graph-palette-utils";
+import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils";
+import {DndModule} from "ngx-drag-drop";
+import { MenuListNg2Module } from "app/ng2/components/downgrade-wrappers/menu-list-ng2/menu-list-ng2.module";
+import { UiElementsModule } from "app/ng2/components/ui/ui-elements.module";
+import {ServicePathSelectorModule} from "./service-path-selector/service-path-selector.module";
+import {SdcUiComponentsModule, SdcUiServices} from "onap-ui-angular";
+import {CanvasSearchModule} from "./canvas-search/canvas-search.module";
+import {CompositionGraphLinkUtils, ServicePathGraphUtils} from "./utils";
+
+
+@NgModule({
+ declarations: [CompositionGraphComponent],
+ imports: [CommonModule,
+ ServicePathSelectorModule,
+ SdcUiComponentsModule,
+ MenuListNg2Module,
+ UiElementsModule,
+ ZoneModules,
+ CanvasSearchModule,
+ DndModule],
+ exports: [CompositionGraphComponent],
+ entryComponents: [CompositionGraphComponent],
+ providers: [
+ CompositionGraphZoneUtils,
+ CompositionGraphGeneralUtils,
+ MatchCapabilitiesRequirementsUtils,
+ CompositionGraphNodesUtils,
+ CompositionGraphLinkUtils,
+ CommonGraphUtils,
+ NodesFactory,
+ LinksFactory,
+ ImageCreatorService,
+ ConnectionWizardService,
+ CompositionGraphPaletteUtils,
+ QueueServiceUtils,
+ SdcUiServices.simplePopupMenuService,
+ ServicePathGraphUtils
+ ]
+})
+export class CompositionGraphModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.html
new file mode 100644
index 0000000000..b24e469554
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.html
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright (C) 2018 AT&T Intellectual Property. All rights reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<div>
+ <connection-wizard-header currentStepIndex="2"></connection-wizard-header>
+ <properties-step></properties-step>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.less
new file mode 100644
index 0000000000..07f9aa2135
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.less
@@ -0,0 +1,4 @@
+connection-wizard-header {
+ display: block;
+ margin-bottom: 15px;
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.ts
new file mode 100644
index 0000000000..5abb879013
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.ts
@@ -0,0 +1,10 @@
+import {Component} from "@angular/core";
+
+
+@Component({
+ selector: 'connection-properties-view',
+ templateUrl: './connection-properties-view.component.html',
+ styleUrls:['./connection-properties-view.component.less']
+})
+export class ConnectionPropertiesViewComponent {
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.html
new file mode 100644
index 0000000000..7e7e82d85f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.html
@@ -0,0 +1,52 @@
+<!--
+ ~ Copyright (C) 2018 AT&T Intellectual Property. All rights reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<div class="header-main-container">
+ <div class="inner-container">
+ <div class="node from-node" [ngClass]="{'selected':currentStepIndex == 0}">
+ <div class="text">
+ <div class="node-name">
+ {{connectWizardService.connectRelationModel.fromNode.componentInstance.name}}
+ </div>
+ <div class="selected-req-or-cap" [ngClass]="{'selected': currentStepIndex == 2 && !connectWizardService.selectedMatch.isFromTo}">
+ {{getSelectedReqOrCapName(true)}}
+ </div>
+ </div>
+ <div class="icon">
+ <div class="small medium {{connectWizardService.connectRelationModel.fromNode.componentInstance.iconSprite}} {{connectWizardService.connectRelationModel.fromNode.componentInstance.icon}}">
+ </div>
+ </div>
+ </div>
+ <div class="connection">
+
+ </div>
+ <div class="node to-node" [ngClass]="{'selected':currentStepIndex == 1}">
+ <div class="icon">
+ <div class="small medium {{connectWizardService.connectRelationModel.toNode.componentInstance.iconSprite}} {{connectWizardService.connectRelationModel.toNode.componentInstance.icon}}">
+ </div>
+ </div>
+
+ <div class="text">
+ <div class="node-name">
+ {{connectWizardService.connectRelationModel.toNode.componentInstance.name}}
+ </div>
+ <div class="selected-req-or-cap" [ngClass]="{'selected': currentStepIndex == 2 && connectWizardService.selectedMatch.isFromTo}">
+ {{getSelectedReqOrCapName(false)}}
+ </div>
+ </div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.less
new file mode 100644
index 0000000000..d8bab288d3
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.less
@@ -0,0 +1,53 @@
+@import '../../../../../../../assets/styles/sprite-proxy-services-icons';
+@import '../../../../../../../assets/styles/variables';
+.header-main-container{
+ background-color: #f8f8f8;
+ width: 100%;
+ height: 100px;
+ display: flex;
+ .inner-container{
+ margin: 0 auto;
+ display: flex;
+ }
+}
+.selected {
+ color: @main_color_a;
+}
+.node{
+ display: flex;
+ &.from-node{
+ text-align: right;
+ }
+ &.to-node{
+ text-align: left;
+ }
+ &.selected{
+ .icon{
+ border: solid 3px @main_color_a;
+ padding: 4px;
+ border-radius: 50%;
+ background-color: @main_color_p;
+ }
+ }
+ .icon{
+ margin: auto 0;
+ display: flex;
+ }
+ .text{
+ font-family: @font-opensans-medium;
+ margin: auto 10px;
+ min-width: 450px;
+ .node-name{
+ font-size: 11px;
+ }
+ .selected-req-or-cap{
+ font-size: 14px;
+ }
+ }
+}
+.connection{
+ width: 67px;
+ height: 0px;
+ border-bottom: dashed 2px #979797;
+ margin: auto 0;
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts
new file mode 100644
index 0000000000..f5bc3b7ca4
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts
@@ -0,0 +1,37 @@
+/**
+ * Created by rc2122 on 9/27/2017.
+ */
+import {Component, Inject, forwardRef} from "@angular/core";
+import {ConnectionWizardService} from "../connection-wizard.service";
+import {WizardHeaderBaseComponent} from "app/ng2/components/ui/multi-steps-wizard/multi-steps-wizard-header-base.component";
+
+@Component({
+ selector: 'connection-wizard-header',
+ templateUrl: './connection-wizard-header.component.html',
+ styleUrls:['./connection-wizard-header.component.less']
+})
+
+export class ConnectionWizardHeaderComponent extends WizardHeaderBaseComponent{
+
+ constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) {
+ super();
+ }
+
+ private _getReqOrCapName(isFromNode:boolean) {
+ const attributeReqOrCap:string = isFromNode ? 'requirement' : 'capability';
+ if (this.connectWizardService.selectedMatch[attributeReqOrCap]) {
+ return this.connectWizardService.selectedMatch[attributeReqOrCap].getTitle();
+ } else if (this.connectWizardService.selectedMatch.relationship) {
+ return this.connectWizardService.selectedMatch.relationship.relation[attributeReqOrCap];
+ }
+ return '';
+ }
+
+ private getSelectedReqOrCapName = (isFromNode:boolean):string => {
+ if(!this.connectWizardService.selectedMatch){
+ return '';
+ }
+ return this._getReqOrCapName(this.connectWizardService.selectedMatch.isFromTo ? isFromNode : !isFromNode);
+ }
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts
new file mode 100644
index 0000000000..80464dc970
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts
@@ -0,0 +1,43 @@
+import {ToNodeStepComponent} from "./to-node-step/to-node-step.component";
+import {NgModule} from "@angular/core";
+import {FromNodeStepComponent} from "./from-node-step/from-node-step.component";
+import {PropertiesStepComponent} from "./properties-step/properties-step.component";
+import {ConnectionWizardService} from "./connection-wizard.service";
+import {SelectRequirementOrCapabilityModule} from "../../../../components/logic/select-requirement-or-capability/select-requirement-or-capability.module";
+import {PropertyTableModule} from "../../../../components/logic/properties-table/property-table.module";
+import {FormElementsModule} from "../../../../components/ui/form-components/form-elements.module";
+import {ConnectionWizardHeaderComponent} from "./connection-wizard-header/connection-wizard-header.component";
+import {ConnectionPropertiesViewComponent} from "./connection-properties-view/connection-properties-view.component";
+import {BrowserModule} from "@angular/platform-browser";
+
+@NgModule({
+ declarations: [
+ FromNodeStepComponent,
+ ToNodeStepComponent,
+ PropertiesStepComponent,
+ ConnectionWizardHeaderComponent,
+ ConnectionPropertiesViewComponent
+ ],
+ imports: [
+ FormElementsModule,
+ PropertyTableModule,
+ SelectRequirementOrCapabilityModule,
+ BrowserModule
+ ],
+ exports: [
+ FromNodeStepComponent,
+ ToNodeStepComponent,
+ PropertiesStepComponent,
+ ConnectionWizardHeaderComponent,
+ ConnectionPropertiesViewComponent
+ ],
+ entryComponents: [FromNodeStepComponent,
+ ToNodeStepComponent,
+ PropertiesStepComponent,
+ ConnectionWizardHeaderComponent,
+ ConnectionPropertiesViewComponent
+ ],
+ providers: [ConnectionWizardService]
+})
+export class ConnectionWizardModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.spec.ts
new file mode 100644
index 0000000000..8a5c5fcefb
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.spec.ts
@@ -0,0 +1,85 @@
+import {TestBed} from "@angular/core/testing";
+import {WorkspaceService} from "../../../../pages/workspace/workspace.service";
+import { ConnectionWizardService } from "app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service";
+import { ConnectRelationModel, Match, Requirement, Capability } from "app/models";
+import { Mock } from "ts-mockery/dist";
+
+describe('Connection Wizard Service', () => {
+
+ let service: ConnectionWizardService;
+
+ const connectRelationModelMock = Mock.of<ConnectRelationModel>({
+ possibleRelations: [
+ Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement1', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability1', type: 'othertype'})}),
+ Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement2', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability2', type: 'tosca'})}),
+ Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement3', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability3', type: 'tosca'})}),
+ Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement4', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability2', type: 'tosca'})}),
+ Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement5', capability: "cap2"}), capability: Mock.of<Capability>({uniqueId: 'capability1', type: 'tosca'})}),
+ Mock.of<Match>({isFromTo: false, requirement: Mock.of<Requirement>({uniqueId: 'requirement6', capability: "cap2"}), capability: Mock.of<Capability>({uniqueId: 'capability2', type: 'tosca'})}),
+ Mock.of<Match>({isFromTo: false, requirement: Mock.of<Requirement>({uniqueId: 'requirement7', capability: "cap2"}), capability: Mock.of<Capability>({uniqueId: 'capability1', type: 'othertype'})})
+ ]
+ });
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [],
+ providers: [ConnectionWizardService,
+ {provide: WorkspaceService, useValue: {}}
+ ]
+ });
+
+ service = TestBed.get(ConnectionWizardService);
+ service.connectRelationModel = connectRelationModelMock;
+ });
+
+ describe('getOptionalRequirementsByInstanceUniqueId', () => {
+ it('if no capability to match is sent in and isFromTo is true, ALL isFromTo==true requirements are returned', () => {
+ const requirements = service.getOptionalRequirementsByInstanceUniqueId(true);
+ expect(requirements['cap1'].length).toBe(4);
+ expect(requirements['cap2'].length).toBe(1);
+ });
+
+ it('if no capability to match is sent in and isFromTo is false, ALL isFromTo==false requirements are returned', () => {
+ const requirements = service.getOptionalRequirementsByInstanceUniqueId(false);
+ expect(requirements['cap1']).toBeUndefined();
+ expect(requirements['cap2'].length).toBe(2);
+ });
+
+ it('if capability to match IS sent in and isFromTo is true, matches with the same uniqueID and isFromTo==true are returned', () => {
+ const capability = Mock.of<Capability>({uniqueId: 'capability1'});
+ const requirements = service.getOptionalRequirementsByInstanceUniqueId(true, capability);
+ expect(requirements['cap1'].length).toBe(1);
+ expect(requirements['cap2'].length).toBe(1);
+ });
+
+ it('if capability to match IS sent in and isFromTo is false, requirements with the same uniqueID and isFromTo==false are returned', () => {
+ const capability = Mock.of<Capability>({uniqueId: 'capability1'});
+ const requirements = service.getOptionalRequirementsByInstanceUniqueId(false, capability);
+ expect(requirements['cap1']).toBeUndefined();
+ expect(requirements['cap2'].length).toBe(1);
+ });
+ })
+
+ describe('getOptionalCapabilitiesByInstanceUniqueId', () => {
+ it('if requirement to match IS sent in and isFromTo is true, matches with the same uniqueID and isFromTo==true are returned', () => {
+ const requirement = Mock.of<Requirement>({uniqueId: 'requirement1'});
+ const capabilities = service.getOptionalCapabilitiesByInstanceUniqueId(true, requirement);
+ expect(capabilities['othertype'].length).toBe(1);
+ expect(capabilities['tosca']).toBeUndefined();
+ });
+
+ it('if no requirement to match is sent in and isFromTo is true, a UNIQUE list of all capabilities with isFromTo==true are returned', () => {
+ const capabilities = service.getOptionalCapabilitiesByInstanceUniqueId(true);
+ expect(capabilities['othertype'].length).toBe(1);
+ expect(capabilities['tosca'].length).toBe(2);
+ });
+
+ it('if no requirement to match is sent in and isFromTo is false, all capabilities with isFromTo==false are returned', () => {
+ const capabilities = service.getOptionalCapabilitiesByInstanceUniqueId(false);
+ expect(capabilities['othertype'].length).toBe(1);
+ expect(capabilities['tosca'].length).toBe(1);
+ });
+ });
+
+});
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.ts
new file mode 100644
index 0000000000..2eb5428f61
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.ts
@@ -0,0 +1,58 @@
+import * as _ from "lodash";
+import {ConnectRelationModel} from "app/models/graph/connectRelationModel";
+import {Injectable} from "@angular/core";
+import { Requirement, Capability} from "app/models";
+import {Dictionary} from "lodash";
+import {Match, Component, PropertyFEModel} from "app/models";
+import {Store} from "@ngxs/store";
+import {WorkspaceService} from "../../../workspace/workspace.service";
+
+@Injectable()
+export class ConnectionWizardService {
+
+ connectRelationModel:ConnectRelationModel;
+ selectedMatch:Match;
+ changedCapabilityProperties:PropertyFEModel[];
+
+
+ constructor(private workspaceService: WorkspaceService) {
+ this.changedCapabilityProperties = [];
+
+ }
+
+ public setRelationMenuDirectiveObj = (connectRelationModel:ConnectRelationModel) => {
+ this.connectRelationModel = connectRelationModel;
+ // this.selectedCapability = rel
+ }
+
+ getOptionalRequirementsByInstanceUniqueId = (isFromTo: boolean, matchWith?:Capability): Dictionary<Requirement[]> => {
+ let requirements: Array<Requirement> = [];
+ _.forEach(this.connectRelationModel.possibleRelations, (match: Match) => {
+ if(!matchWith || match.capability.uniqueId == matchWith.uniqueId){
+ if(match.isFromTo == isFromTo){
+ requirements.push(match.requirement);
+ }
+ }
+ });
+ requirements = _.uniqBy(requirements, (req:Requirement)=>{
+ return req.ownerId + req.uniqueId + req.name;
+ });
+ return _.groupBy(requirements, 'capability');
+ }
+
+ getOptionalCapabilitiesByInstanceUniqueId = (isFromTo: boolean, matchWith?:Requirement): Dictionary<Capability[]> => {
+ let capabilities: Array<Capability> = [];
+ _.forEach(this.connectRelationModel.possibleRelations, (match: Match) => {
+ if(!matchWith || match.requirement.uniqueId == matchWith.uniqueId){
+ if(match.isFromTo == isFromTo){
+ capabilities.push(match.capability);
+ }
+ }
+ });
+ capabilities = _.uniqBy(capabilities, (cap:Capability)=>{
+ return cap.ownerId + cap.uniqueId + cap.name;
+ });
+ return _.groupBy(capabilities, 'type');
+ }
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/__snapshots__/from-node-step.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/__snapshots__/from-node-step.component.spec.ts.snap
new file mode 100644
index 0000000000..739ce3d8fe
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/__snapshots__/from-node-step.component.spec.ts.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`from-node-step component should match current snapshot 1`] = `
+<from-node-step
+ connectWizardService={[Function Object]}
+ preventBack={[Function Function]}
+ preventNext={[Function Function]}
+ updateSelectedReqOrCap={[Function Function]}
+>
+ <select-requirement-or-capability />
+</from-node-step>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.html
new file mode 100644
index 0000000000..0a70069748
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.html
@@ -0,0 +1,22 @@
+<!--
+ ~ Copyright (C) 2018 AT&T Intellectual Property. All rights reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<select-requirement-or-capability [optionalRequirementsMap]="optionalRequirementsMap"
+ [optionalCapabilitiesMap]="optionalCapabilitiesMap"
+ [selectedReqOrCapModel]="connectWizardService.selectedMatch && (connectWizardService.selectedMatch.isFromTo ? connectWizardService.selectedMatch.requirement : connectWizardService.selectedMatch.capability)"
+ [componentInstanceId]="connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId"
+ (updateSelectedReqOrCap)="updateSelectedReqOrCap($event)">
+</select-requirement-or-capability> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.spec.ts
new file mode 100644
index 0000000000..59ff72adda
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.spec.ts
@@ -0,0 +1,114 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { Capability, Match } from 'app/models';
+import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper';
+import { Requirement } from '../../../../../../models/requirement';
+import { ConnectionWizardService } from '../connection-wizard.service';
+import { FromNodeStepComponent } from './from-node-step.component';
+
+describe('from-node-step component', () => {
+
+ let fixture: ComponentFixture<FromNodeStepComponent>;
+ let connectionWizardServiceMockWithoutSelectedMatch: Partial<ConnectionWizardService>;
+ let connectionWizardServiceMockWithSelectedMatch: Partial<ConnectionWizardService>;
+
+ const connectionWizardServiceMockSelectedMatchWithRequirements = {requirement: 'val'};
+
+ connectionWizardServiceMockWithoutSelectedMatch = {
+ getOptionalRequirementsByInstanceUniqueId: jest.fn().mockReturnValue(5),
+ getOptionalCapabilitiesByInstanceUniqueId: jest.fn().mockReturnValue(10),
+
+ connectRelationModel: {
+ fromNode: {
+ componentInstance: {
+ uniqueId : 'testUniqueID'
+ }
+ }
+ }
+ };
+
+ connectionWizardServiceMockWithSelectedMatch = {
+ selectedMatch: connectionWizardServiceMockSelectedMatchWithRequirements,
+ getOptionalRequirementsByInstanceUniqueId: jest.fn().mockReturnValue(5),
+ getOptionalCapabilitiesByInstanceUniqueId: jest.fn().mockReturnValue(10)
+ };
+
+ let expectedConnectionWizardServiceMock = connectionWizardServiceMockWithoutSelectedMatch;
+
+ beforeEach(
+ async(() => {
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [FromNodeStepComponent],
+ imports: [],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: ConnectionWizardService, useValue: expectedConnectionWizardServiceMock}
+ ],
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(FromNodeStepComponent);
+ });
+ })
+ );
+
+
+ it('should match current snapshot', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+ it('preventBack return true - always', () => {
+ fixture.componentInstance.ngOnInit();
+ const result = fixture.componentInstance.preventBack();
+ expect(result).toEqual(true);
+ });
+
+ it('preventNext return true since selectedMatch does not exist in connectionWizardServiceMock', () => {
+ fixture.componentInstance.ngOnInit();
+ const result = fixture.componentInstance.preventNext();
+ expect(result).toEqual(true);
+ });
+
+ it('preventNext return false since to selectedMatch or selectedMatch.capability & selectedMatch.requirement does exist in connectionWizardServiceMock', () => {
+ fixture.componentInstance.connectWizardService = connectionWizardServiceMockWithSelectedMatch;
+ fixture.componentInstance.ngOnInit();
+ const result = fixture.componentInstance.preventNext();
+ expect(result).toEqual(false);
+ });
+
+ it('updateSelectedReqOrCap is called with instance of requirement, the selectMatch will be set to an Instance of Match of type Requirement', () => {
+ const requirement = new Requirement();
+ fixture.componentInstance.updateSelectedReqOrCap(requirement);
+ const expectedSelectedMatch = fixture.componentInstance.connectWizardService.selectedMatch;
+
+ expect(expectedSelectedMatch).toBeInstanceOf(Match);
+ expect(expectedSelectedMatch.capability).toBe(null);
+ expect(expectedSelectedMatch.fromNode).toBe('testUniqueID');
+ expect(expectedSelectedMatch.isFromTo).toBe(true);
+ expect(expectedSelectedMatch.toNode).toBe(null);
+ expect(expectedSelectedMatch.requirement).toBeInstanceOf(Requirement);
+ });
+
+ it('updateSelectedReqOrCap is called with instance of capability, the selectMatch will be set to an Instance of Match of type Capability', () => {
+ const capability = new Capability();
+ fixture.componentInstance.updateSelectedReqOrCap(capability);
+ const expectedSelectedMatch = fixture.componentInstance.connectWizardService.selectedMatch;
+
+ expect(expectedSelectedMatch).toBeInstanceOf(Match);
+ expect(expectedSelectedMatch.requirement).toBe(null);
+ expect(expectedSelectedMatch.fromNode).toBe(null);
+ expect(expectedSelectedMatch.isFromTo).toBe(false);
+ expect(expectedSelectedMatch.toNode).toBe('testUniqueID');
+ expect(expectedSelectedMatch.capability).toBeInstanceOf(Capability);
+ });
+
+ it('updateSelectedReqOrCap is called with null, the selectMatch will be set to null', () => {
+ fixture.componentInstance.updateSelectedReqOrCap(null);
+ const expectedSelectedMatch = fixture.componentInstance.connectWizardService.selectedMatch;
+
+ expect(expectedSelectedMatch).toBe(null);
+ });
+
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.ts
new file mode 100644
index 0000000000..cffd58c9ea
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.ts
@@ -0,0 +1,44 @@
+import { Component, forwardRef, Inject, OnInit } from '@angular/core';
+import { Match } from 'app/models';
+import { Capability } from 'app/models/capability';
+import { Requirement } from 'app/models/requirement';
+import { IStepComponent } from 'app/models/wizard-step';
+import { Dictionary } from 'lodash';
+import { ConnectionWizardService } from '../connection-wizard.service';
+
+@Component({
+ selector: 'from-node-step',
+ templateUrl: './from-node-step.component.html'
+})
+
+export class FromNodeStepComponent implements IStepComponent, OnInit{
+
+ optionalRequirementsMap: Dictionary<Requirement[]>;
+ optionalCapabilitiesMap: Dictionary<Capability[]>;
+
+ constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) {}
+
+ ngOnInit() {
+ this.optionalRequirementsMap = this.connectWizardService.getOptionalRequirementsByInstanceUniqueId(true);
+ this.optionalCapabilitiesMap = this.connectWizardService.getOptionalCapabilitiesByInstanceUniqueId(false);
+ }
+
+ preventNext = (): boolean => {
+ return !this.connectWizardService.selectedMatch || (!this.connectWizardService.selectedMatch.capability && !this.connectWizardService.selectedMatch.requirement);
+ }
+
+ preventBack = (): boolean => {
+ return true;
+ }
+
+ private updateSelectedReqOrCap = (selected: Requirement|Capability): void => {
+ if (!selected) {
+ this.connectWizardService.selectedMatch = null;
+ } else if (selected instanceof Requirement) {
+ this.connectWizardService.selectedMatch = new Match(<Requirement>selected, null, true, this.connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId, null);
+ } else {
+ this.connectWizardService.selectedMatch = new Match(null, <Capability>selected , false, null, this.connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId);
+ }
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.html
new file mode 100644
index 0000000000..a8177595a5
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.html
@@ -0,0 +1,28 @@
+<!--
+ ~ Copyright (C) 2018 AT&T Intellectual Property. All rights reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<div class="title">
+ <span class="capability-name">
+ {{(connectWizardService.selectedMatch.capability && connectWizardService.selectedMatch.capability.getTitle()) || connectWizardService.selectedMatch.relationship.relation.capability}}
+ </span>
+</div>
+<div class="properties-table-container">
+ <properties-table class="properties-table"
+ (propertyChanged)="propertyValueChanged($event)"
+ [fePropertiesMap]="capabilityPropertiesMap"
+ [selectedPropertyId]="''"
+ [hidePropertyType]="true">
+ </properties-table>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.less
new file mode 100644
index 0000000000..c8ad4d38d2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.less
@@ -0,0 +1,15 @@
+@import '../../../../../../../assets/styles/variables';
+.title{
+ margin-bottom: 20px;
+ .capability-name-label{
+ font-size: 13px;
+ }
+ .capability-name{
+ font-family: @font-opensans-medium;
+ color: @main_color_a;
+ }
+}
+.properties-table-container{
+ height: 362px;
+ overflow-y: auto;
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.ts
new file mode 100644
index 0000000000..2c12e0daed
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.ts
@@ -0,0 +1,68 @@
+/**
+ * Created by ob0695 on 9/4/2017.
+ */
+/**
+ * Created by rc2122 on 9/4/2017.
+ */
+import {Component, Inject, forwardRef} from '@angular/core';
+import {IStepComponent} from "app/models"
+import {ConnectionWizardService} from "../connection-wizard.service";
+import {PropertyFEModel} from "app/models/properties-inputs/property-fe-model";
+import {InstanceFePropertiesMap} from "app/models/properties-inputs/property-fe-map";
+import {PropertiesUtils} from "app/ng2/pages/properties-assignment/services/properties.utils";
+import { ComponentInstanceServiceNg2 } from "app/ng2/services/component-instance-services/component-instance.service";
+
+@Component({
+ selector: 'properties-step',
+ templateUrl: './properties-step.component.html',
+ styleUrls: ['./properties-step.component.less']
+})
+
+export class PropertiesStepComponent implements IStepComponent{
+
+ capabilityPropertiesMap: InstanceFePropertiesMap;
+ savingProperty:boolean = false;
+
+ constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService, private componentInstanceServiceNg2:ComponentInstanceServiceNg2, private propertiesUtils:PropertiesUtils) {
+
+ this.capabilityPropertiesMap = this.propertiesUtils.convertPropertiesMapToFEAndCreateChildren({'capability' : connectWizardService.selectedMatch.capability.properties}, false);
+ }
+
+ ngOnInit() {
+ this.connectWizardService.changedCapabilityProperties = [];
+ }
+
+ onPropertySelectedUpdate = ($event) => {
+ console.log("==>" + 'PROPERTY VALUE SELECTED');
+ // this.selectedFlatProperty = $event;
+ // let parentProperty:PropertyFEModel = this.propertiesService.getParentPropertyFEModelFromPath(this.instanceFePropertiesMap[this.selectedFlatProperty.instanceName], this.selectedFlatProperty.path);
+ // parentProperty.expandedChildPropertyId = this.selectedFlatProperty.path;
+ };
+
+ propertyValueChanged = (property: PropertyFEModel) => {
+ if (!property.isDeclared) {
+ const propChangedIdx = this.connectWizardService.changedCapabilityProperties.indexOf(property);
+ if (property.hasValueObjChanged()) {
+ // if (this.componentInstanceServiceNg2.hasPropertyChanged(property)) {
+ console.log("==>" + this.constructor.name + ": propertyValueChanged " + property);
+ if (propChangedIdx === -1) {
+ this.connectWizardService.changedCapabilityProperties.push(property);
+ }
+ }
+ else {
+ if (propChangedIdx !== -1) {
+ console.log("==>" + this.constructor.name + ": propertyValueChanged (reset to original) " + property);
+ this.connectWizardService.changedCapabilityProperties.splice(propChangedIdx, 1);
+ }
+ }
+ }
+ };
+
+ preventNext = ():boolean => {
+ return false;
+ }
+
+ preventBack = ():boolean => {
+ return this.savingProperty;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/__snapshots__/to-node-step.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/__snapshots__/to-node-step.component.spec.ts.snap
new file mode 100644
index 0000000000..ea587bce71
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/__snapshots__/to-node-step.component.spec.ts.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`to-node-step component should match current snapshot 1`] = `
+<to-node-step
+ connectWizardService={[Function Object]}
+ optionalCapabilitiesMap={[Function Object]}
+ optionalRequirementsMap={[Function Object]}
+ preventBack={[Function Function]}
+ preventNext={[Function Function]}
+ updateSelectedReqOrCap={[Function Function]}
+>
+ <select-requirement-or-capability />
+</to-node-step>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.html
new file mode 100644
index 0000000000..4892b7fadc
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.html
@@ -0,0 +1,22 @@
+<!--
+ ~ Copyright (C) 2018 AT&T Intellectual Property. All rights reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<select-requirement-or-capability [optionalRequirementsMap]="optionalRequirementsMap"
+ [optionalCapabilitiesMap]="optionalCapabilitiesMap"
+ [selectedReqOrCapModel]="connectWizardService.selectedMatch.isFromTo ? connectWizardService.selectedMatch.capability : connectWizardService.selectedMatch.requirement"
+ [selectedReqOrCapOption]="displayRequirementsOrCapabilities"
+ [componentInstanceId]="connectWizardService.connectRelationModel.toNode.componentInstance.uniqueId"
+ (updateSelectedReqOrCap)="updateSelectedReqOrCap($event)">
+</select-requirement-or-capability> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.spec.ts
new file mode 100644
index 0000000000..9d453f21dd
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.spec.ts
@@ -0,0 +1,71 @@
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {NO_ERRORS_SCHEMA} from "@angular/core";
+import {ToNodeStepComponent} from "./to-node-step.component";
+import {ConnectionWizardService} from "../connection-wizard.service";
+import {ConfigureFn, configureTests} from "../../../../../../../jest/test-config.helper";
+import {Match} from "../../../../../../models/graph/match-relation";
+
+
+describe('to-node-step component', () => {
+
+ let fixture: ComponentFixture<ToNodeStepComponent>;
+ let connectionWizardServiceMock: Partial<ConnectionWizardService>;
+
+ beforeEach(
+ async(() => {
+
+ connectionWizardServiceMock = {
+ // selectedMatch: new Match(null, null, true, '',''),
+ selectedMatch: {
+ isFromTo: false
+ },
+ getOptionalRequirementsByInstanceUniqueId: jest.fn().mockReturnValue(5),
+ getOptionalCapabilitiesByInstanceUniqueId: jest.fn().mockReturnValue(10)
+ }
+
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [ToNodeStepComponent],
+ imports: [],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: ConnectionWizardService, useValue: connectionWizardServiceMock}
+ ],
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(ToNodeStepComponent);
+ });
+ })
+ );
+
+
+ it('should match current snapshot', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+ it('should test the ngOnInit with isFromTo = false', () => {
+ const component = TestBed.createComponent(ToNodeStepComponent);
+ let service = TestBed.get(ConnectionWizardService);
+ service.selectedMatch.isFromTo = false;
+ component.componentInstance.ngOnInit();
+ expect(component.componentInstance.displayRequirementsOrCapabilities).toEqual("Requirement");
+ expect(connectionWizardServiceMock.getOptionalRequirementsByInstanceUniqueId).toHaveBeenCalledWith(false, connectionWizardServiceMock.selectedMatch.capability);
+ expect(component.componentInstance.optionalRequirementsMap).toEqual(5);
+ expect(component.componentInstance.optionalCapabilitiesMap).toEqual({});
+ });
+
+
+ it('should test the ngOnInit with isFromTo = true', () => {
+ const component = TestBed.createComponent(ToNodeStepComponent);
+ let service = TestBed.get(ConnectionWizardService);
+ service.selectedMatch.isFromTo = true;
+ component.componentInstance.ngOnInit();
+ expect(component.componentInstance.displayRequirementsOrCapabilities).toEqual("Capability");
+ expect(connectionWizardServiceMock.getOptionalCapabilitiesByInstanceUniqueId).toHaveBeenCalledWith(true, connectionWizardServiceMock.selectedMatch.requirement);
+ expect(component.componentInstance.optionalCapabilitiesMap).toEqual(10);
+ expect(component.componentInstance.optionalRequirementsMap).toEqual({});
+ });
+
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.ts
new file mode 100644
index 0000000000..67dc381284
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.ts
@@ -0,0 +1,65 @@
+import {Component, forwardRef, Inject} from '@angular/core';
+import {IStepComponent} from "app/models"
+import {Dictionary} from "lodash";
+import {ConnectionWizardService} from "../connection-wizard.service";
+import {Match} from "app/models/graph/match-relation";
+import {Requirement} from "app/models/requirement";
+import {Capability} from "app/models/capability";
+import {PropertyModel} from "app/models/properties";
+
+@Component({
+ selector: 'to-node-step',
+ templateUrl: './to-node-step.component.html'
+})
+
+export class ToNodeStepComponent implements IStepComponent{
+
+ displayRequirementsOrCapabilities:string; //get 'Requirement' or 'Capability'
+ optionalRequirementsMap: Dictionary<Requirement[]> = {};
+ optionalCapabilitiesMap: Dictionary<Capability[]> ={};
+
+ constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) {
+ }
+
+ ngOnInit(){
+ if(this.connectWizardService.selectedMatch.isFromTo){
+ this.displayRequirementsOrCapabilities = 'Capability';
+ this.optionalRequirementsMap = {};
+ this.optionalCapabilitiesMap = this.connectWizardService.getOptionalCapabilitiesByInstanceUniqueId(true, this.connectWizardService.selectedMatch.requirement);
+ }else{
+ this.displayRequirementsOrCapabilities = 'Requirement';
+ this.optionalRequirementsMap = this.connectWizardService.getOptionalRequirementsByInstanceUniqueId(false, this.connectWizardService.selectedMatch.capability);
+ this.optionalCapabilitiesMap = {}
+ }
+
+
+ }
+
+ preventNext = ():boolean => {
+ return !this.connectWizardService.selectedMatch.capability || !this.connectWizardService.selectedMatch.requirement;
+ }
+
+ preventBack = ():boolean => {
+ return false;
+ }
+
+ private updateSelectedReqOrCap = (selected:Requirement|Capability):void => {
+ if (!selected) {
+ if (this.connectWizardService.selectedMatch.isFromTo) {
+ this.connectWizardService.selectedMatch.capability = undefined;
+ this.connectWizardService.selectedMatch.toNode = undefined;
+ } else {
+ this.connectWizardService.selectedMatch.requirement = undefined;
+ this.connectWizardService.selectedMatch.fromNode = undefined;
+ }
+ } else if (selected instanceof Requirement) {
+ this.connectWizardService.selectedMatch.requirement = <Requirement>selected;
+ this.connectWizardService.selectedMatch.fromNode = this.connectWizardService.connectRelationModel.toNode.componentInstance.uniqueId;
+ } else {
+ this.connectWizardService.selectedMatch.capability = <Capability>selected;
+ this.connectWizardService.selectedMatch.toNode = this.connectWizardService.connectRelationModel.toNode.componentInstance.uniqueId;
+ }
+ this.connectWizardService.selectedMatch.relationship = undefined;
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/__snapshots__/link-row.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/__snapshots__/link-row.component.spec.ts.snap
new file mode 100644
index 0000000000..094f41bd84
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/__snapshots__/link-row.component.spec.ts.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`artifact form component should match current snapshot of artifact form component 1`] = `
+<link-row
+ source={[Function Array]}
+ srcCP={[Function Array]}
+ target={[Function Array]}
+ targetCP={[Function Array]}
+>
+ <ui-element-dropdown
+ class="cell link-selector"
+ data-tests-id="linkSrc"
+ /><ui-element-dropdown
+ class="cell link-selector"
+ data-tests-id="linkSrcCP"
+ /><ui-element-dropdown
+ class="cell link-selector"
+ data-tests-id="linkTarget"
+ /><ui-element-dropdown
+ class="cell link-selector"
+ data-tests-id="linkTargetCP"
+ /><div
+ class="cell remove"
+ data-tests-id="removeLnk"
+ >
+
+ </div>
+</link-row>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.html
new file mode 100644
index 0000000000..0abdda1cc6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.html
@@ -0,0 +1,61 @@
+<!--
+ ~ Copyright (C) 2018 AT&T Intellectual Property. All rights reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<ui-element-dropdown
+ data-tests-id="linkSrc"
+ [readonly]="!link.isFirst || (link.isFirst && !link.canEdit)"
+ class="cell link-selector"
+ [values]="source"
+ [(value)]="link.fromNode"
+ (valueChange)="onSourceSelected($event)">
+</ui-element-dropdown>
+
+<ui-element-dropdown
+ data-tests-id="linkSrcCP"
+ [readonly]="!link.isFirst || (link.isFirst && !link.canEdit)"
+ class="cell link-selector"
+ [values]="srcCP"
+ [(value)]="link.fromCP"
+ (valueChange)="onSrcCPSelected($event)">
+</ui-element-dropdown>
+
+<ui-element-dropdown
+ data-tests-id="linkTarget"
+ [readonly]="!link.canEdit"
+ class="cell link-selector"
+ [values]="target"
+ [(value)]="link.toNode"
+ (valueChange)="onTargetSelected($event)">
+</ui-element-dropdown>
+
+<ui-element-dropdown
+ data-tests-id="linkTargetCP"
+ [readonly]="!link.canEdit"
+ class="cell link-selector"
+ [values]="targetCP"
+ [(value)]="link.toCP"
+ (valueChange)="onTargetCPSelected($event)">
+</ui-element-dropdown>
+
+<div
+ class="cell remove"
+ data-tests-id="removeLnk">
+ <span
+ *ngIf="link.canRemove"
+ class="sprite-new delete-item-icon"
+ (click)="removeRow()">
+ </span>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.less
new file mode 100644
index 0000000000..2a1d0d98c8
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.less
@@ -0,0 +1,21 @@
+@import './../../../../../../../assets/styles/variables.less';
+.remove {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.cell {
+ padding: 0;
+}
+
+/deep/ .link-selector {
+ select {
+ height: 30px;
+ border: none;
+ stroke: none;
+ }
+
+}
+
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.spec.ts
new file mode 100644
index 0000000000..5cbad6ea5d
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.spec.ts
@@ -0,0 +1,478 @@
+import {async, ComponentFixture} from "@angular/core/testing";
+import {CacheService} from "../../../../../services/cache.service";
+import {ConfigureFn, configureTests} from "../../../../../../../jest/test-config.helper";
+import {NO_ERRORS_SCHEMA} from "@angular/core";
+import {LinkRowComponent} from "./link-row.component";
+import {DropdownValue} from "../../../../../components/ui/form-components/dropdown/ui-element-dropdown.component";
+import {MapItemData, ServicePathMapItem} from "../../../../../../models/graph/nodes-and-links-map";
+
+describe('artifact form component', () => {
+
+ let fixture: ComponentFixture<LinkRowComponent>;
+ let cacheServiceMock: Partial<CacheService>;
+
+ beforeEach(
+ async(() => {
+
+
+ cacheServiceMock = {
+ contains: jest.fn(),
+ remove: jest.fn(),
+ set: jest.fn(),
+ get: jest.fn()
+ }
+
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [LinkRowComponent],
+ imports: [],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: []
+ ,
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(LinkRowComponent);
+ });
+ })
+ );
+
+
+ it('should match current snapshot of artifact form component', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+
+ it('ngOnChanges() -> in case data exist -> call to parseInitialData()' ,() => {
+ // init values / mock functions
+ let data = 'something';
+ fixture.componentInstance.parseInitialData = jest.fn();
+ fixture.componentInstance.data = data;
+
+ // call to the tested function
+ fixture.componentInstance.ngOnChanges();
+
+ // expect that
+ expect(fixture.componentInstance.parseInitialData).toHaveBeenCalledWith(data);
+ });
+
+ it('onSourceSelected() -> in case id -> srcCP, link.fromCP, link.toNode, link.toCP, target, targetCP should be updated accordingly' ,() => {
+ // init values / mock functions
+ let id = 'id';
+ let data = 'data';
+ let link = {
+ fromCP:'testVal',
+ toNode:'testVal',
+ toCP:'testVal'
+ }
+ let target = ['val1', 'val2'];
+ let targetCP = ['val1', 'val2'];
+
+ fixture.componentInstance.findOptions = jest.fn();
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal');
+ fixture.componentInstance.data = data;
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.target = target;
+ fixture.componentInstance.targetCP = targetCP;
+
+ // call to the tested function
+ fixture.componentInstance.onSourceSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.findOptions).toHaveBeenCalledWith(data, id);
+ expect(fixture.componentInstance.srcCP).toBe('dummyConvertedVal');
+ expect(fixture.componentInstance.link.fromCP).toBe('');
+ expect(fixture.componentInstance.link.toNode).toBe('');
+ expect(fixture.componentInstance.link.toCP).toBe('');
+ expect(fixture.componentInstance.target.length).toBe(0);
+ expect(fixture.componentInstance.targetCP.length).toBe(0);
+ });
+
+ it('onSourceSelected() -> in case id undefined -> No Change to srcCP, link.fromCP, link.toNode, link.toCP, target, targetCP' ,() => {
+ // init values / mock functions
+ let id;
+ let data = 'data';
+ let link = {
+ fromCP:'testVal',
+ toNode:'testVal',
+ toCP:'testVal'
+ }
+ let target = ['val1', 'val2'];
+ let targetCP = ['val1', 'val2'];
+
+ fixture.componentInstance.findOptions = jest.fn();
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal');
+ fixture.componentInstance.data = data;
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.target = target;
+ fixture.componentInstance.targetCP = targetCP;
+
+ // call to the tested function
+ fixture.componentInstance.onSourceSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.link.fromCP).toBe(link.fromCP);
+ expect(fixture.componentInstance.link.toNode).toBe(link.toNode);
+ expect(fixture.componentInstance.link.toCP).toBe(link.toCP);
+ expect(fixture.componentInstance.target.length).toBe(2);
+ expect(fixture.componentInstance.target[0]).toBe('val1')
+ expect(fixture.componentInstance.targetCP.length).toBe(2);
+ expect(fixture.componentInstance.targetCP[1]).toBe('val2');
+ });
+
+ it('onSrcCPSelected() -> in case id -> Verify target, link.fromCPOriginId, link.toNode, link.toCP, targetCP.length' ,() => {
+ // init values / mock functions
+ let id = 'id';
+ let link = {
+ fromNode:'testVal',
+ toCPOriginId: 'initValue_ShouldBeChanged'
+ };
+ let option1 = {
+ id: 'something'
+ };
+ let option2 = {
+ id: 'id',
+ data: {"ownerId":1}
+ };
+
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.findOptions = jest.fn(() => [option1, option2]);
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal');
+
+ // call to the tested function
+ fixture.componentInstance.onSrcCPSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.target).toBe('dummyConvertedVal');
+ expect(fixture.componentInstance.link.fromCPOriginId).toBe(option2.data.ownerId);
+ expect(fixture.componentInstance.link.toNode).toBe('');
+ expect(fixture.componentInstance.link.toCP).toBe('');
+ expect(fixture.componentInstance.targetCP.length).toBe(0);
+
+ });
+
+ it('onSrcCPSelected() -> in case id undefined -> Verify target, link.fromCPOriginId, link.toNode, link.toCP, targetCP.length' ,() => {
+ // init values / mock functions
+ let id;
+
+ let targetInput:Array<DropdownValue> = [{value:'Value', label:'Label', hidden:true, selected:true}];
+
+ let linkInput = {
+ fromCPOriginId:'expectedLinkFromCPOriginId',
+ toNode:'expectedLinkToNode',
+ toCP:'expectedLinkToCP',
+ // Link Object
+ canEdit:true,
+ canRemove:true,
+ isFirst:true,
+ // ForwardingPathLink Object
+ ownerId:'',
+ fromNode:'',
+ fromCP:'',
+ toCPOriginId:''
+ }
+
+ fixture.componentInstance.target = targetInput;
+ fixture.componentInstance.link = linkInput;
+ fixture.componentInstance.targetCP = targetInput;
+
+
+ // call to the tested function
+ fixture.componentInstance.onSrcCPSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.target).toBe(targetInput);
+ expect(fixture.componentInstance.link.fromCPOriginId).toBe('expectedLinkFromCPOriginId');
+ expect(fixture.componentInstance.link.toNode).toBe('expectedLinkToNode');
+ expect(fixture.componentInstance.link.toCP).toBe('expectedLinkToCP');
+ expect(fixture.componentInstance.targetCP.length).toBe(1);
+ });
+
+ it('onTargetSelected() -> in case id -> Verify targetCP & link.toCP' ,() => {
+ // init values / mock functions
+ let id = 'id';
+ let link = {
+ toCP:'testVal'
+ }
+ let targetCP = ['val1', 'val2'];
+
+ fixture.componentInstance.findOptions = jest.fn();
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal');
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.targetCP = targetCP;
+
+ // call to the tested function
+ fixture.componentInstance.onTargetSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.targetCP).toBe('dummyConvertedVal');
+ expect(fixture.componentInstance.link.toCP).toBe('');
+
+ });
+
+ it('onTargetSelected() -> in case id undefined -> Verify targetCP & link.toCP' ,() => {
+ // init values / mock functions
+ let id;
+ let link = {
+ toCP:'toCP_testVal'
+ }
+ let targetCP = ['val1', 'val2'];
+
+ fixture.componentInstance.findOptions = jest.fn();
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal');
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.targetCP = targetCP;
+
+ // call to the tested function
+ fixture.componentInstance.onTargetSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.targetCP.length).toBe(2);
+ expect(fixture.componentInstance.targetCP).toEqual(['val1', 'val2']);
+ expect(fixture.componentInstance.link.toCP).toBe('toCP_testVal');
+ });
+
+ it('onTargetCPSelected() -> in case id -> Validate toCPOriginId' ,() => {
+ // init values / mock functions
+ let id = 'id';
+ let link = {
+ toNode:'testVal',
+ toCPOriginId: 'initValue_ShouldBeChanged'
+ };
+ let option1 = {
+ id: 'something'
+ };
+ let option2 = {
+ id: 'id',
+ data: {"ownerId":1}
+ };
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.findOptions = jest.fn(() => [option1, option2]);
+
+ // call to the tested function
+ fixture.componentInstance.onTargetCPSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.link.toCPOriginId).toBe(option2.data.ownerId);
+ });
+
+ it('onTargetCPSelected() -> in case id undefined -> Validate toCPOriginId' ,() => {
+ // init values / mock functions
+ let id;
+ let link = {
+ toNode:'testVal',
+ toCPOriginId: 'initValue_ShouldRemain'
+ };
+ let option1 = {
+ id: 'something'
+ };
+ let option2 = {
+ id: 'id',
+ data: {"ownerId":1}
+ };
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.findOptions = jest.fn(() => [option1, option2]);
+
+ // call to the tested function
+ fixture.componentInstance.onTargetCPSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.link.toCPOriginId).toBe('initValue_ShouldRemain');
+ });
+
+
+ it('findOptions() -> in case item.data.options -> Validate return item.data.options' ,() => {
+ // init values / mock functions
+ const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []};
+ const innerServicePathItem: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 };
+ const mapItemData1: MapItemData = { id: 'mapItemData1_id', name: 'mapItemData1_name', options: [innerServicePathItem]};
+
+ const servicePathItem: ServicePathMapItem = { id: 'servicePathItem_id', data: mapItemData1 };
+ const arrServicePathItems: ServicePathMapItem[] = [servicePathItem];
+
+ let nodeOrCPId: string = servicePathItem.id;
+
+ // call to the tested function
+ let res = fixture.componentInstance.findOptions(arrServicePathItems, nodeOrCPId);
+
+ // expect that
+ expect(res).toEqual([innerServicePathItem]);
+ });
+
+ it('findOptions() -> in case NOT item || item.data || item.data.options -> Validate return null' ,() => {
+ // init values / mock functions
+ let item = [{
+ // data: {
+ data:{
+ name:'data_name',
+ id: 'data_id'
+ },
+ name:'name',
+ id: 'id'
+ // }
+ }];
+ let items: Array<ServicePathMapItem> = item;
+ let nodeOrCPId: string = 'someString';
+
+ // call to the tested function
+ let res = fixture.componentInstance.findOptions(items, nodeOrCPId);
+
+ // expect that
+ expect(res).toBe(null);
+ });
+
+ it('convertValuesToDropDownOptions() -> Verify that the result is sorted' ,() => {
+ // init values / mock functions
+ const mapItemData1: MapItemData = { id: 'Z_ID', name: 'Z_NAME'};
+ const servicePathItem1: ServicePathMapItem = { id: 'Z_servicePathItem_id', data: mapItemData1 };
+
+ const mapItemData2: MapItemData = { id: 'A_ID', name: 'A_NAME'};
+ const servicePathItem2: ServicePathMapItem = { id: 'A_servicePathItem_id', data: mapItemData2 };
+
+ const mapItemData3: MapItemData = { id: 'M_ID', name: 'M_NAME'};
+ const servicePathItem3: ServicePathMapItem = { id: 'M_servicePathItem_id', data: mapItemData3 };
+
+ const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1, servicePathItem2, servicePathItem3];
+
+ // call to the tested function
+ let res = fixture.componentInstance.convertValuesToDropDownOptions(arrServicePathItems);
+
+ // expect that
+ expect(res.length).toBe(3);
+ expect(res[0].value).toBe("A_servicePathItem_id");
+ expect(res[0].label).toBe("A_NAME");
+ expect(res[1].value).toBe("M_servicePathItem_id");
+ expect(res[1].label).toBe("M_NAME");
+ expect(res[2].value).toBe("Z_servicePathItem_id");
+ expect(res[2].label).toBe("Z_NAME");
+
+ });
+
+ it('parseInitialData() -> link.fromNode Exist => Verify srcCP' ,() => {
+ // init values / mock functions
+
+ //Simulate Array<ServicePathMapItem to pass to the function
+ const mapItemData1: MapItemData = { id: 'mapItemID', name: 'mapItemName'};
+ const servicePathItem1: ServicePathMapItem = { id: 'servicePathItemId', data: mapItemData1 };
+ const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1];
+
+ //Simulate link
+ let link = {
+ fromNode:'testVal'
+ };
+ fixture.componentInstance.link = link;
+
+ //Simulate the response from convertValuesToDropDownOptions()
+ const value = "expected_id_fromNode";
+ const label = "expected_label_fromNode"
+ let result:Array<DropdownValue> = [];
+ result[0] = new DropdownValue(value, label);
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => result);
+
+ //Simulate the response from findOptions()
+ const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []};
+ const options: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 };
+ fixture.componentInstance.findOptions = jest.fn(() => options);
+
+
+ // call to the tested function
+ fixture.componentInstance.parseInitialData(arrServicePathItems);
+
+ // expect that
+ expect(fixture.componentInstance.srcCP.length).toBe(1);
+ expect(fixture.componentInstance.srcCP[0]).toEqual({
+ "value": value,
+ "label": label,
+ "hidden": false,
+ "selected": false
+ });
+ });
+
+ it('parseInitialData() -> link.fromNode & link.fromCP Exist => Verify srcCP' ,() => {
+ // init values / mock functions
+
+ //Simulate Array<ServicePathMapItem to pass to the function
+ const mapItemData1: MapItemData = { id: 'mapItemID', name: 'mapItemName'};
+ const servicePathItem1: ServicePathMapItem = { id: 'servicePathItemId', data: mapItemData1 };
+ const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1];
+
+ //Simulate link
+ let link = {
+ fromNode:'testVal',
+ fromCP: 'testVal'
+ };
+ fixture.componentInstance.link = link;
+
+ //Simulate the response from convertValuesToDropDownOptions()
+ const value = "expected_id_fromNode_and_fromCP";
+ const label = "expected_label_fromNode_and_fromCP"
+ let result:Array<DropdownValue> = [];
+ result[0] = new DropdownValue(value, label);
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => result);
+
+ //Simulate the response from findOptions()
+ const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []};
+ const options: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 };
+ fixture.componentInstance.findOptions = jest.fn(() => options);
+
+
+ // call to the tested function
+ fixture.componentInstance.parseInitialData(arrServicePathItems);
+
+ // expect that
+ expect(fixture.componentInstance.srcCP.length).toBe(1);
+ expect(fixture.componentInstance.srcCP[0]).toEqual({
+ "value": value,
+ "label": label,
+ "hidden": false,
+ "selected": false
+ });
+ });
+
+
+ it('parseInitialData() -> link.fromNode & link.fromCP & link.toNode Exist => Verify srcCP' ,() => {
+ // init values / mock functions
+
+ //Simulate Array<ServicePathMapItem to pass to the function
+ const mapItemData1: MapItemData = { id: 'mapItemID', name: 'mapItemName'};
+ const servicePathItem1: ServicePathMapItem = { id: 'servicePathItemId', data: mapItemData1 };
+ const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1];
+
+ //Simulate link
+ let link = {
+ fromNode:'testVal',
+ fromCP: 'testVal',
+ toNode: 'testVal'
+ };
+ fixture.componentInstance.link = link;
+
+ //Simulate the response from convertValuesToDropDownOptions()
+ const value = "expected_id_fromNode_and_fromCP_and_toNode";
+ const label = "expected_label_fromNode_and_fromCP_and_toNode"
+ let result:Array<DropdownValue> = [];
+ result[0] = new DropdownValue(value, label);
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => result);
+
+ //Simulate the response from findOptions()
+ const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []};
+ const options: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 };
+ fixture.componentInstance.findOptions = jest.fn(() => options);
+
+
+ // call to the tested function
+ fixture.componentInstance.parseInitialData(arrServicePathItems);
+
+ // expect that
+ expect(fixture.componentInstance.srcCP.length).toBe(1);
+ expect(fixture.componentInstance.srcCP[0]).toEqual({
+ "value": value,
+ "label": label,
+ "hidden": false,
+ "selected": false
+ });
+ });
+
+
+
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.ts
new file mode 100644
index 0000000000..83c30b1a60
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.ts
@@ -0,0 +1,104 @@
+import {Component, Input} from '@angular/core';
+import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component";
+import {Link} from './link.model';
+import {ServicePathMapItem} from "app/models/graph/nodes-and-links-map";
+import * as _ from "lodash";
+
+@Component({
+ selector: 'link-row',
+ templateUrl: './link-row.component.html',
+ styleUrls: ['./link-row.component.less']
+})
+
+
+export class LinkRowComponent {
+ @Input() data:Array<ServicePathMapItem>;
+ @Input() link:Link;
+ @Input() removeRow:Function;
+ source: Array<DropdownValue> = [];
+ target: Array<DropdownValue> = [];
+ srcCP: Array<DropdownValue> = [];
+ targetCP: Array<DropdownValue> = [];
+
+ ngOnChanges() {
+ if (this.data) {
+ this.parseInitialData(this.data);
+ }
+ }
+
+ parseInitialData(data: Array<ServicePathMapItem>) {
+ this.source = this.convertValuesToDropDownOptions(data);
+ if (this.link.fromNode) {
+ let srcCPOptions = this.findOptions(data, this.link.fromNode);
+ if (!srcCPOptions) { return; }
+ this.srcCP = this.convertValuesToDropDownOptions(srcCPOptions);
+ if (this.link.fromCP) {
+ this.target = this.convertValuesToDropDownOptions(data);
+ if (this.link.toNode) {
+ let targetCPOptions = this.findOptions(data, this.link.toNode);
+ if (!targetCPOptions) { return; }
+ this.targetCP = this.convertValuesToDropDownOptions(targetCPOptions);
+ }
+ }
+ }
+ }
+
+ private findOptions(items: Array<ServicePathMapItem>, nodeOrCPId: string) {
+ let item = _.find(items, (dataItem) => nodeOrCPId === dataItem.id);
+ if (item && item.data && item.data.options) {
+ return item.data.options;
+ }
+ console.warn('no option was found to match selection of Node/CP with id:' + nodeOrCPId);
+ return null;
+ }
+
+ private convertValuesToDropDownOptions(values: Array<ServicePathMapItem>): Array<DropdownValue> {
+ let result:Array<DropdownValue> = [];
+ for (let i = 0; i < values.length ; i++) {
+ result[result.length] = new DropdownValue(values[i].id, values[i].data.name);
+ }
+ return result.sort((a, b) => a.label.localeCompare(b.label));
+ }
+
+ onSourceSelected(id) {
+ if (id) {
+ let srcCPOptions = this.findOptions(this.data, id);
+ this.srcCP = this.convertValuesToDropDownOptions(srcCPOptions);
+ this.link.fromCP = '';
+ this.link.toNode = '';
+ this.link.toCP = '';
+ this.target = [];
+ this.targetCP = [];
+ }
+ }
+
+ onSrcCPSelected (id) {
+ if (id) {
+ let srcCPOptions = this.findOptions(this.data, this.link.fromNode);
+ let srcCPData = srcCPOptions.find(option => id === option.id).data;
+ this.target = this.convertValuesToDropDownOptions(this.data);
+ this.link.fromCPOriginId = srcCPData.ownerId;
+ this.link.toNode = '';
+ this.link.toCP = '';
+ this.targetCP = [];
+ }
+
+ }
+
+ onTargetSelected(id) {
+ if (id) {
+ let targetCPOptions = this.findOptions(this.data, id);
+ this.targetCP = this.convertValuesToDropDownOptions(targetCPOptions);
+ this.link.toCP = '';
+ }
+
+ }
+
+ onTargetCPSelected(id) {
+ if (id) {
+ let targetCPOptions = this.findOptions(this.data, this.link.toNode);
+ let targetCPDataObj = targetCPOptions.find(option => id === option.id).data;
+ this.link.toCPOriginId = targetCPDataObj.ownerId;
+ }
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link.model.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link.model.ts
new file mode 100644
index 0000000000..80128eb42e
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link.model.ts
@@ -0,0 +1,36 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+'use strict';
+import {ForwardingPathLink} from "app/models/forwarding-path-link";
+
+export class Link extends ForwardingPathLink {
+ public canEdit:boolean = false;
+ public canRemove:boolean = false;
+ public isFirst:boolean = false;
+
+ constructor(link: ForwardingPathLink, canEdit: boolean, canRemove: boolean, isFirst: boolean) {
+ super(link.fromNode,link.fromCP, link.toNode, link.toCP, link.fromCPOriginId, link.toCPOriginId);
+ this.canEdit = canEdit;
+ this.canRemove = canRemove;
+ this.isFirst = isFirst;
+ }
+}
+
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.html
new file mode 100644
index 0000000000..db0d912934
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.html
@@ -0,0 +1,55 @@
+<!--
+ ~ Copyright (C) 2018 AT&T Intellectual Property. All rights reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<div class="service-path-creator">
+ <form class="w-sdc-form">
+ <div class="i-sdc-form-item" >
+ <label class="i-sdc-form-label required">Flow Name</label>
+ <input type="text" data-tests-id="pathName" name="pathName" [(ngModel)]="forwardingPath.name" [attr.maxLength]="200" />
+ </div>
+
+ <div class="side-by-side">
+ <div class="i-sdc-form-item" >
+ <label class="i-sdc-form-label">Protocol</label>
+ <input type="text" data-tests-id="pathProtocol" name="protocol" [(ngModel)]="forwardingPath.protocol" [attr.maxLength]="200" />
+ </div>
+ <div class="i-sdc-form-item" >
+ <label class="i-sdc-form-label">Destination Port Numbers</label>
+ <input type="text" data-tests-id="pathPortNumbers" name="portNumbers" [(ngModel)]="forwardingPath.destinationPortNumber" pattern="[0-9,]*" />
+ </div>
+ </div>
+
+ <div class="separator-buttons">
+ <span class="based-on-title">Based On</span>
+ <a (click)="addRow()" [ngClass]="{'disabled':!isExtendAllowed()}" data-tests-id="extendPathlnk">Extend Flow</a>
+ </div>
+
+ <div class="generic-table">
+ <div class="header-row">
+ <div class="cell header-cell" *ngFor="let header of headers">
+ {{header}}
+ </div>
+ </div>
+ <div *ngIf="links && links.length === 0" class="no-row-text" >
+ There is no data to display
+ </div>
+ <div>
+ <link-row *ngFor="let link of links" [data]="linksMap" [link]="link" [removeRow]="removeRow" class="data-row" ></link-row>
+ </div>
+ </div>
+
+
+ </form>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.less
new file mode 100644
index 0000000000..2a3efbdd3c
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.less
@@ -0,0 +1,45 @@
+@import './../../../../../../assets/styles/variables.less';
+.service-path-creator {
+ font-family: @font-opensans-regular;
+ .separator-buttons {
+ margin: 10px 0;
+ display: flex;
+ justify-content: space-between;
+ }
+ .i-sdc-form-label {
+ font-size: 12px;
+ }
+ .w-sdc-form .i-sdc-form-item {
+ margin-bottom: 15px;
+ }
+
+ .side-by-side {
+ display: flex;
+ .i-sdc-form-item {
+ flex-basis: 100%;
+ &:first-child {
+ margin-right: 10px;
+ }
+ }
+ }
+
+ .generic-table {
+ max-height: 233px;
+ .header-row .header-cell {
+ &:last-child {
+ padding: 0;
+ }
+ }
+ /deep/ .cell {
+ &:last-child {
+ min-width: 30px;
+ }
+ }
+ }
+
+ .based-on-title {
+ text-transform: uppercase;
+ font-size: 18px;
+ font-family: @font-opensans-regular;
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.ts
new file mode 100644
index 0000000000..17c2081a75
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.ts
@@ -0,0 +1,149 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+import * as _ from "lodash";
+import { Component, ElementRef, forwardRef, Inject } from '@angular/core';
+import {Link} from './link-row/link.model';
+import {ForwardingPath} from 'app/models/forwarding-path';
+import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service";
+import {ForwardingPathLink} from "app/models/forwarding-path-link";
+import {ServicePathMapItem} from "app/models/graph/nodes-and-links-map";
+import {CompositionService} from "app/ng2/pages/composition/composition.service";
+
+@Component({
+ selector: 'service-path-creator',
+ templateUrl: './service-path-creator.component.html',
+ styleUrls:['./service-path-creator.component.less'],
+ providers: [ServiceServiceNg2]
+})
+
+export class ServicePathCreatorComponent {
+
+ linksMap:Array<ServicePathMapItem>;
+ links:Array<Link> = [];
+ input:any;
+ headers: Array<string> = [];
+ removeRow: Function;
+ forwardingPath:ForwardingPath;
+ //isExtendAllowed:boolean = false;
+
+ constructor(private serviceService: ServiceServiceNg2,
+ private compositionService: CompositionService) {
+ this.forwardingPath = new ForwardingPath();
+ this.links = [new Link(new ForwardingPathLink('', '', '', '', '', ''), true, false, true)];
+ this.headers = ['Source', 'Source Connection Point', 'Target', 'Target Connection Point', ' '];
+ this.removeRow = () => {
+ if (this.links.length === 1) {
+ return;
+ }
+ this.links.splice(this.links.length-1, 1);
+ this.enableCurrentRow();
+ };
+ }
+
+ ngOnInit() {
+ this.serviceService.getNodesAndLinksMap(this.input.serviceId).subscribe((res:any) => {
+ this.linksMap = res;
+ });
+ this.processExistingPath();
+
+ }
+
+ private processExistingPath() {
+ if (this.input.pathId) {
+ let forwardingPath = <ForwardingPath>{...this.compositionService.forwardingPaths[this.input.pathId]};
+ this.forwardingPath.name = forwardingPath.name;
+ this.forwardingPath.destinationPortNumber = forwardingPath.destinationPortNumber;
+ this.forwardingPath.protocol = forwardingPath.protocol;
+ this.forwardingPath.uniqueId = forwardingPath.uniqueId;
+ this.links = [];
+ _.forEach(forwardingPath.pathElements.listToscaDataDefinition, (link:ForwardingPathLink) => {
+ this.links[this.links.length] = new Link(link, false, false, false);
+ });
+ this.links[this.links.length - 1].canEdit = true;
+ this.links[this.links.length - 1].canRemove = true;
+ this.links[0].isFirst = true;
+ }
+ }
+
+ isExtendAllowed():boolean {
+ if (this.links[this.links.length-1].toCP) {
+ return true;
+ }
+ return false;
+ }
+
+ enableCurrentRow() {
+ this.links[this.links.length-1].canEdit = true;
+ if (this.links.length !== 1) {
+ this.links[this.links.length-1].canRemove = true;
+ }
+ }
+
+ addRow() {
+ this.disableRows();
+ this.links[this.links.length] = new Link(
+ new ForwardingPathLink(this.links[this.links.length-1].toNode,
+ this.links[this.links.length-1].toCP,
+ '',
+ '',
+ this.links[this.links.length-1].toCPOriginId,
+ ''
+ ),
+ true,
+ true,
+ false
+ );
+ }
+
+ disableRows() {
+ for (let i = 0 ; i < this.links.length ; i++) {
+ this.links[i].canEdit = false;
+ this.links[i].canRemove = false;
+ }
+ }
+
+ createPathLinksObject() {
+ for (let i = 0 ; i < this.links.length ; i++) {
+ let link = this.links[i];
+ this.forwardingPath.addPathLink(link.fromNode, link.fromCP, link.toNode, link.toCP, link.fromCPOriginId, link.toCPOriginId);
+ }
+ }
+
+ createServicePathData() {
+ this.createPathLinksObject();
+ return this.forwardingPath;
+ }
+
+ checkFormValidForSubmit():boolean {
+ if (this.forwardingPath.name && this.isPathValid() ) {
+ return true;
+ }
+ return false;
+ }
+
+ isPathValid():boolean {
+ let lastLink = this.links[this.links.length -1] ;
+ if (lastLink.toNode && lastLink.toCP && lastLink.fromNode && lastLink.fromCP) {
+ return true;
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.module.ts
new file mode 100644
index 0000000000..78005317a2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.module.ts
@@ -0,0 +1,25 @@
+import { NgModule } from "@angular/core";
+import {CommonModule} from "@angular/common";
+import {ServicePathCreatorComponent} from "./service-path-creator.component";
+import {FormsModule} from "@angular/forms";
+import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module";
+import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module";
+import {LinkRowComponent} from './link-row/link-row.component'
+@NgModule({
+ declarations: [
+ ServicePathCreatorComponent,
+ LinkRowComponent
+ ],
+ imports: [CommonModule,
+ FormsModule,
+ FormElementsModule,
+ UiElementsModule
+ ],
+ exports: [],
+ entryComponents: [
+ ServicePathCreatorComponent
+ ],
+ providers: []
+})
+export class ServicePathCreatorModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.html
new file mode 100644
index 0000000000..e1a4f68a9b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.html
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright (C) 2018 AT&T Intellectual Property. All rights reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<div class="service-path-selector">
+ <label>Service Flows:</label>
+ <ui-element-dropdown
+ class="path-dropdown"
+ data-tests-id="service-path-selector"
+ [readonly]="dropdownOptions.length < 3"
+ [(value)]="selectedPathId"
+ [values]="dropdownOptions"
+ (valueChange)="onSelectPath()">
+ </ui-element-dropdown>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.less
new file mode 100644
index 0000000000..f618d6b6f4
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.less
@@ -0,0 +1,24 @@
+@import './../../../../../../assets/styles/variables.less';
+.service-path-selector {
+ margin: 10px 35px 10px 0;
+ display: flex;
+ font-size: 12px;
+
+ /deep/ .path-dropdown {
+ width: 150px;
+ select {
+ font-size: 14px;
+ font-family: @font-opensans-regular;
+ padding: 4px 10px;
+ }
+ }
+
+ label {
+ margin-right: 10px;
+ align-self: center;
+ font-size: 14px;
+ font-family: @font-opensans-regular;
+ font-weight: normal;
+ margin-bottom: initial;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.ts
new file mode 100644
index 0000000000..0dba906f64
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.ts
@@ -0,0 +1,142 @@
+import {Component, Input, KeyValueDiffer, IterableDiffers, KeyValueDiffers, DoCheck} from '@angular/core';
+import {Service} from "app/models/components/service";
+import {TranslateService} from "app/ng2/shared/translator/translate.service";
+import {ForwardingPath} from "app/models/forwarding-path";
+import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component";
+import {CompositionService} from "app/ng2/pages/composition/composition.service";
+import {EventListenerService} from "app/services/event-listener-service";
+import {GRAPH_EVENTS} from "app/utils/constants";
+
+@Component({
+ selector: 'service-path-selector',
+ templateUrl: './service-path-selector.component.html',
+ styleUrls: ['service-path-selector.component.less']
+})
+
+export class ServicePathSelectorComponent {
+
+ defaultSelectedId: string;
+ hideAllValue: string;
+ hideAllId: string = '0';
+ showAllValue: string;
+ showAllId: string = '1';
+
+ paths: Array<ForwardingPath> = [];
+ dropdownOptions: Array<DropdownValue>;
+ differ: KeyValueDiffer<string, ForwardingPath>;
+
+ @Input() drawPath: Function;
+ @Input() deletePaths: Function;
+ @Input() selectedPathId: string;
+
+ constructor(private differs: KeyValueDiffers,
+ private translateService: TranslateService,
+ private compositionService: CompositionService,
+ private eventListenerService: EventListenerService
+ ) {
+
+ this.defaultSelectedId = this.hideAllId;
+ this.convertPathsToDropdownOptions();
+
+ this.translateService.languageChangedObservable.subscribe(lang => {
+ this.hideAllValue = this.translateService.translate("SERVICE_PATH_SELECTOR_HIDE_ALL_VALUE");
+ this.showAllValue = this.translateService.translate("SERVICE_PATH_SELECTOR_SHOW_ALL_VALUE");
+ this.convertPathsToDropdownOptions();
+ });
+
+ }
+
+ ngOnInit(): void {
+
+ this.selectedPathId = this.defaultSelectedId;
+ this.differ = this.differs.find(this.compositionService.forwardingPaths).create();
+ this.updatePaths();
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_SERVICE_PATH_CREATED, (createdId) => {
+ this.selectedPathId = createdId;
+ this.updatePaths();
+ } )
+
+ }
+
+ updatePaths(): void {
+
+ const pathsChanged = this.differ.diff(this.compositionService.forwardingPaths);
+
+ if (pathsChanged) {
+ let oldPaths = _.cloneDeep(this.paths);
+ this.populatePathsFromService();
+
+ if (!(_.isEqual(oldPaths, this.paths))) {
+ this.convertPathsToDropdownOptions();
+
+ let temp = this.selectedPathId;
+ this.selectedPathId = '-1';
+
+ setTimeout(() => {
+ this.selectedPathId = temp;
+ this.onSelectPath();
+ }, 0);
+ }
+ }
+
+ }
+
+ populatePathsFromService(): void {
+
+ this.paths = [];
+
+ _.forEach(this.compositionService.forwardingPaths, path => {
+ this.paths.push(path);
+ });
+ this.paths.sort((a: ForwardingPath, b: ForwardingPath) => {
+ return a.name.localeCompare(b.name);
+ });
+
+ }
+
+ convertPathsToDropdownOptions(): void {
+
+ let result = [
+ new DropdownValue(this.hideAllId, this.hideAllValue),
+ new DropdownValue(this.showAllId, this.showAllValue)
+ ];
+
+ _.forEach(this.paths, (value: ForwardingPath) => {
+ result[result.length] = new DropdownValue(value.uniqueId, value.name);
+ });
+
+ this.dropdownOptions = result;
+
+ }
+
+ onSelectPath = (): void => {
+
+ if (this.selectedPathId !== '-1') {
+ this.deletePaths();
+
+ switch (this.selectedPathId) {
+ case this.hideAllId:
+ break;
+
+ case this.showAllId:
+ _.forEach(this.paths, path =>
+ this.drawPath(path)
+ );
+ break;
+
+ default:
+ let path = this.paths.find(path =>
+ path.uniqueId === this.selectedPathId
+ );
+ if (!path) {
+ this.selectedPathId = this.defaultSelectedId;
+ this.onSelectPath(); // currently does nothing in default case, but if one day it does, we want the selection to behave accordingly.
+ break;
+ }
+ this.drawPath(path);
+ break;
+ }
+ }
+
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.module.ts
new file mode 100644
index 0000000000..6782c88b76
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.module.ts
@@ -0,0 +1,22 @@
+import { NgModule } from "@angular/core";
+import {CommonModule} from "@angular/common";
+import {ServicePathSelectorComponent} from "./service-path-selector.component";
+import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module";
+import {CompositionService} from "app/ng2/pages/composition/composition.service";
+
+@NgModule({
+ declarations: [
+ ServicePathSelectorComponent
+ ],
+ imports: [
+ CommonModule,
+ UiElementsModule
+ ],
+ exports: [ServicePathSelectorComponent],
+ entryComponents: [
+ ServicePathSelectorComponent
+ ],
+ providers: [CompositionService]
+})
+export class ServicePathSelectorModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.html
new file mode 100644
index 0000000000..39c41916a2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.html
@@ -0,0 +1,21 @@
+<div class="service-path-list">
+ <div class="add-path-link" *ngIf="!isViewOnly"><a (click)="onAddServicePath()" data-tests-id="add-service-path-lnk" >+ Add Flow</a></div>
+ <div class="generic-table table-container" >
+ <div class="header-row">
+ <div class="cell header-cell" *ngFor="let header of headers">
+ {{header}}
+ </div>
+ </div>
+ <div *ngFor="let path of paths" class="data-row" >
+ <div class="cell" data-tests-id="path-name" >{{path.name}}</div>
+ <div class="cell path-action-buttons">
+ <span class="sprite-new update-component-icon" (click)="onEditServicePath(path.uniqueId)" data-tests-id="update-service-path-btn" ></span>
+ <span class="sprite-new delete-item-icon" *ngIf="!isViewOnly" (click)="deletePath(path.uniqueId)" data-tests-id="delete-service-path-btn"></span>
+ </div>
+ </div>
+ <div *ngIf="paths && paths.length === 0" class="no-row-text" >
+ No flows have been added yet.
+ </div>
+ </div>
+
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.less
new file mode 100644
index 0000000000..17f70926ff
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.less
@@ -0,0 +1,24 @@
+@import './../../../../../../assets/styles/variables.less';
+
+.add-path-link {
+ display: flex;
+ align-items: flex-end;
+ flex-direction: column;
+ padding-bottom: 10px;
+}
+
+.generic-table {
+ max-height: 233px;
+}
+
+.path-action-buttons {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ .sprite-new {
+ cursor: pointer;
+ }
+ & > span:only-child {
+ margin: auto;
+}
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.ts
new file mode 100644
index 0000000000..81abe42cb3
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.ts
@@ -0,0 +1,70 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+import * as _ from "lodash";
+import {Component, ComponentRef} from '@angular/core';
+import {ForwardingPath} from "app/models/forwarding-path";
+import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service";
+import {ModalService} from "app/ng2/services/modal.service";
+import {ModalComponent} from "app/ng2/components/ui/modal/modal.component";
+import {CompositionService} from "app/ng2/pages/composition/composition.service";
+
+@Component({
+ selector: 'service-paths-list',
+ templateUrl: './service-paths-list.component.html',
+ styleUrls:['service-paths-list.component.less'],
+ providers: [ServiceServiceNg2, ModalService]
+})
+export class ServicePathsListComponent {
+ modalInstance: ComponentRef<ModalComponent>;
+ headers: Array<string> = [];
+ paths: Array<ForwardingPath> = [];
+ input:any;
+ onAddServicePath: Function;
+ onEditServicePath: Function;
+ isViewOnly: boolean;
+
+ constructor(private serviceService:ServiceServiceNg2,
+ private compositionService: CompositionService) {
+ this.headers = ['Flow Name','Actions'];
+ }
+
+ ngOnInit() {
+ _.forEach(this.compositionService.forwardingPaths, (path: ForwardingPath)=> {
+ this.paths[this.paths.length] = path;
+ });
+ this.paths.sort((a:ForwardingPath, b:ForwardingPath)=> {
+ return a.name.localeCompare(b.name);
+ });
+ this.onAddServicePath = this.input.onCreateServicePath;
+ this.onEditServicePath = this.input.onEditServicePath;
+ this.isViewOnly = this.input.isViewOnly;
+ }
+
+ deletePath = (id:string):void => {
+ this.serviceService.deleteServicePath(this.input.serviceId, id).subscribe((res:any) => {
+ delete this.compositionService.forwardingPaths[id];
+ this.paths = this.paths.filter(function(path){
+ return path.uniqueId !== id;
+ });
+ });
+ };
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.module.ts
new file mode 100644
index 0000000000..5121627a9d
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.module.ts
@@ -0,0 +1,17 @@
+import { NgModule } from "@angular/core";
+import {CommonModule} from "@angular/common";
+import { ServicePathsListComponent } from "./service-paths-list.component";
+
+@NgModule({
+ declarations: [
+ ServicePathsListComponent
+ ],
+ imports: [CommonModule],
+ exports: [],
+ entryComponents: [
+ ServicePathsListComponent
+ ],
+ providers: []
+})
+export class ServicePathsListModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-general-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-general-utils.ts
new file mode 100644
index 0000000000..bc8bd691c9
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-general-utils.ts
@@ -0,0 +1,268 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+import * as _ from "lodash";
+import {ComponentInstance, Match, CompositionCiLinkBase, CompositionCiNodeUcpeCp} from "app/models";
+import {Dictionary, GraphUIObjects} from "app/utils";
+import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils";
+import {CommonGraphUtils} from "../common/common-graph-utils";
+import {Injectable} from "@angular/core";
+import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils";
+import {ComponentServiceNg2} from "app/ng2/services/component-services/component.service";
+import {RequirementsGroup} from "app/models/requirement";
+import {CapabilitiesGroup} from "app/models/capability";
+import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service";
+import {CompositionService} from "../../composition.service";
+import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service";
+import {NotificationsService} from "onap-ui-angular/dist/notifications/services/notifications.service";
+import {NotificationSettings} from "onap-ui-angular/dist/notifications/utilities/notification.config";
+
+export interface RequirementAndCapabilities {
+ capabilities: CapabilitiesGroup;
+ requirements: RequirementsGroup;
+}
+
+@Injectable()
+export class CompositionGraphGeneralUtils {
+
+ public componentRequirementsAndCapabilitiesCaching = new Dictionary<string, RequirementAndCapabilities>();
+
+ constructor(private commonGraphUtils: CommonGraphUtils,
+ private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils,
+ private queueServiceUtils: QueueServiceUtils,
+ private componentService: ComponentServiceNg2,
+ private topologyTemplateService: TopologyTemplateService,
+ private compositionService: CompositionService,
+ private workspaceService: WorkspaceService) {
+ }
+
+ /**
+ * 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;
+ };
+
+ public zoomGraphTo = (cy: Cy.Instance, zoomLevel: number): void => {
+ let zy = cy.height() / 2;
+ let zx = cy.width() / 2;
+ cy.zoom({
+ level: zoomLevel,
+ renderedPosition: {x: zx, y: zy}
+ });
+ }
+
+ //saves the current zoom, and then sets a temporary maximum zoom for zoomAll, and then reverts to old value
+ public zoomAllWithMax = (cy: Cy.Instance, maxZoom: number): void => {
+
+ let oldMaxZoom: number = cy.maxZoom();
+
+ cy.maxZoom(maxZoom);
+ this.zoomAll(cy);
+ cy.maxZoom(oldMaxZoom);
+
+ };
+
+ //Zooms to fit all of the nodes in the collection passed in. If no nodes are passed in, will zoom to fit all nodes on graph
+ public zoomAll = (cy: Cy.Instance, nodes?: Cy.CollectionNodes): void => {
+
+ if (!nodes || !nodes.length) {
+ nodes = cy.nodes();
+ }
+
+ cy.resize();
+ cy.animate({
+ fit: {eles: nodes, padding: 20},
+ center: {eles: nodes}
+ }, {duration: 400});
+ };
+
+ /**
+ * 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 topologyTemplate instance can be hosted on the UCPE instance
+ * @param cy - Cytoscape instance
+ * @param fromUcpeInstance
+ * @param toComponentInstance
+ * @returns {Match}
+ */
+ public canBeHostedOn(cy: Cy.Instance, fromUcpeInstance: ComponentInstance, toComponentInstance: ComponentInstance): Match {
+
+ let matches: Array<Match> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromUcpeInstance, toComponentInstance, this.getAllCompositionCiLinks(cy));
+ let hostedOnMatch: Match = _.find(matches, (match: Match) => {
+ return match.requirement.capability.toLowerCase() === 'tosca.capabilities.container';
+ });
+
+ return 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: Match = 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) {
+
+ let illegalOverlappingNodes = _.filter(cy.nodes("[isSdcElement]"), (graphNode: Cy.CollectionFirstNode) => {
+ if (this.isBBoxOverlapping(pseudoNodeBBox, graphNode.renderedBoundingBox())) {
+ 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();
+ });
+ };
+
+ /**
+ *
+ * @param blockAction - true/false if this is a block action
+ * @param instances
+ * @param component
+ */
+ public pushMultipleUpdateComponentInstancesRequestToQueue = (instances: Array<ComponentInstance>): void => {
+ this.queueServiceUtils.addNonBlockingUIAction(() => {
+ return new Promise<boolean>((resolve, reject) => {
+ let uniqueId = this.workspaceService.metadata.uniqueId;
+ let topologyType = this.workspaceService.metadata.componentType;
+ this.topologyTemplateService.updateMultipleComponentInstances(uniqueId, topologyType, instances).subscribe(instancesResult => {
+ this.compositionService.updateComponentInstances(instancesResult);
+ resolve(true);
+ });
+ });
+ });
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-links-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-links-utils.ts
new file mode 100644
index 0000000000..6035d05b7f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-links-utils.ts
@@ -0,0 +1,342 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+/**
+ * Created by obarda on 6/28/2016.
+ */
+import * as _ from "lodash";
+import {GraphUIObjects} from "app/utils";
+import {
+ Match,
+ CompositionCiNodeBase,
+ RelationshipModel,
+ ConnectRelationModel,
+ LinksFactory,
+ Component,
+ LinkMenu,
+ Point,
+ CompositionCiLinkBase,
+ Requirement,
+ Capability,
+ Relationship,
+ ComponentInstance
+} from "app/models";
+import {CommonGraphUtils} from "../common/common-graph-utils";
+import {CompositionGraphGeneralUtils} from "./composition-graph-general-utils";
+import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils";
+import {CompositionCiServicePathLink} from "app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link";
+import {Injectable} from "@angular/core";
+import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils";
+import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service";
+import {SdcUiServices} from "onap-ui-angular";
+import {CompositionService} from "../../composition.service";
+import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service";
+
+@Injectable()
+export class CompositionGraphLinkUtils {
+
+ constructor(private linksFactory: LinksFactory,
+ private generalGraphUtils: CompositionGraphGeneralUtils,
+ private commonGraphUtils: CommonGraphUtils,
+ private queueServiceUtils: QueueServiceUtils,
+ private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils,
+ private topologyTemplateService: TopologyTemplateService,
+ private loaderService: SdcUiServices.LoaderService,
+ private compositionService: CompositionService,
+ private workspaceService: WorkspaceService) {
+
+
+ }
+
+ /**
+ * 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.activate();
+ this.queueServiceUtils.addBlockingUIAction(() => {
+ this.topologyTemplateService.deleteRelation(this.workspaceService.metadata.uniqueId, this.workspaceService.metadata.componentType, link.data().relation).subscribe((deletedRelation) => {
+ this.compositionService.deleteRelation(deletedRelation);
+ cy.remove(link);
+ this.loaderService.deactivate();
+ }, (error) => {this.loaderService.deactivate()});
+ });
+ };
+
+ /**
+ * 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): void => {
+
+ this.loaderService.activate();
+ link.updateLinkDirection();
+
+ this.queueServiceUtils.addBlockingUIAction(() => {
+ this.topologyTemplateService.createRelation(this.workspaceService.metadata.uniqueId, this.workspaceService.metadata.componentType, link.relation).subscribe((relation) => {
+ link.setRelation(relation);
+ this.insertLinkToGraph(cy, link);
+ this.compositionService.addRelation(relation);
+ this.loaderService.deactivate();
+ }, (error) => {this.loaderService.deactivate()})
+ });
+ };
+
+ private createSimpleLink = (match: Match, cy: Cy.Instance): void => {
+ let newRelation: RelationshipModel = match.matchToRelationModel();
+ let linkObg: CompositionCiLinkBase = this.linksFactory.createGraphLink(cy, newRelation, newRelation.relationships[0]);
+ this.createLink(linkObg, cy);
+ };
+
+ public createLinkFromMenu = (cy: Cy.Instance, chosenMatch: Match): void => {
+
+ if (chosenMatch) {
+ if (chosenMatch && chosenMatch instanceof Match) {
+ this.createSimpleLink(chosenMatch, cy);
+ }
+ }
+ }
+
+ /**
+ * 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): ConnectRelationModel {
+
+ let linkModel: Array<CompositionCiLinkBase> = this.generalGraphUtils.getAllCompositionCiLinks(cy);
+
+ let possibleRelations: Array<Match> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromNode.data().componentInstance,
+ toNode.data().componentInstance, linkModel);
+
+ //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 ConnectRelationModel(fromNode.data(), toNode.data(), possibleRelations);
+ }
+ return null;
+ };
+
+ private handlePathLink(cy: Cy.Instance, event: Cy.EventObject) {
+ let linkData = event.cyTarget.data();
+ let selectedPathId = linkData.pathId;
+ let pathEdges = cy.collection(`[pathId='${selectedPathId}']`);
+ if (pathEdges.length > 1) {
+ setTimeout(() => {
+ pathEdges.select();
+ }, 0);
+ }
+ }
+
+ private handleVLLink(event: Cy.EventObject) {
+ let vl: Cy.CollectionNodes = event.cyTarget[0].target('.vl-node');
+ let connectedEdges: Cy.CollectionEdges = vl.connectedEdges(`[type!="${CompositionCiServicePathLink.LINK_TYPE}"]`);
+ if (vl.length && connectedEdges.length > 1) {
+ setTimeout(() => {
+ vl.select();
+ connectedEdges.select();
+ }, 0);
+ }
+ }
+
+
+ /**
+ * Handles click event on links.
+ * If one edge selected: do nothing.
+ * Two 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 > 1 && event.cyTarget[0].selected()) {
+ cy.$(':selected').unselect();
+ } else {
+ if (event.cyTarget[0].data().type === CompositionCiServicePathLink.LINK_TYPE) {
+ this.handlePathLink(cy, event);
+ }
+ else {
+ this.handleVLLink(event);
+ }
+ }
+ }
+
+
+ /**
+ * 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.clientX, event.originalEvent.clientY);
+ 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;
+ };
+
+ /**
+ * Returns relation source and target nodes.
+ * @param nodes - all nodes in graph in order to find the edge connecting the two nodes
+ * @param fromNodeId
+ * @param toNodeId
+ * @returns [source, target] array of source node and target node.
+ */
+ public getRelationNodes(nodes: Cy.CollectionNodes, fromNodeId: string, toNodeId: string) {
+ return [
+ _.find(nodes, (node: Cy.CollectionFirst) => node.data().id === fromNodeId),
+ _.find(nodes, (node: Cy.CollectionFirst) => node.data().id === toNodeId)
+ ];
+ }
+
+
+ /**
+ * go over the relations and draw links on the graph
+ * @param cy
+ * @param getRelationRequirementCapability - function to get requirement and capability of a relation
+ */
+ public initGraphLinks(cy: Cy.Instance, relations: RelationshipModel[]) {
+ if (relations) {
+ _.forEach(relations, (relationshipModel: RelationshipModel) => {
+ _.forEach(relationshipModel.relationships, (relationship: Relationship) => {
+ let linkToCreate = this.linksFactory.createGraphLink(cy, relationshipModel, relationship);
+ this.insertLinkToGraph(cy, linkToCreate);
+ });
+ });
+ }
+ }
+
+ /**
+ * Add link to graph - only draw the link
+ * @param cy
+ * @param link
+ * @param getRelationRequirementCapability
+ */
+ public insertLinkToGraph = (cy: Cy.Instance, link: CompositionCiLinkBase) => {
+ const relationNodes = this.getRelationNodes(cy.nodes(), link.source, link.target);
+ const sourceNode: CompositionCiNodeBase = relationNodes[0] && relationNodes[0].data();
+ const targetNode: CompositionCiNodeBase = relationNodes[1] && relationNodes[1].data();
+ if ((sourceNode && !sourceNode.certified) || (targetNode && !targetNode.certified)) {
+ link.classes = 'not-certified-link';
+ }
+ let linkElement = cy.add({
+ group: 'edges',
+ data: link,
+ classes: link.classes
+ });
+
+ const getLinkRequirementCapability = () =>
+ this.getRelationRequirementCapability(link.relation.relationships[0], sourceNode.componentInstance, targetNode.componentInstance);
+ this.commonGraphUtils.initLinkTooltip(linkElement, link.relation.relationships[0], getLinkRequirementCapability);
+ };
+
+ public syncComponentByRelation(relation: RelationshipModel) {
+ let componentInstances = this.compositionService.getComponentInstances();
+ relation.relationships.forEach((rel) => {
+ if (rel.capability) {
+ const toComponentInstance: ComponentInstance = componentInstances.find((inst) => inst.uniqueId === relation.toNode);
+ const toComponentInstanceCapability: Capability = toComponentInstance.findCapability(
+ rel.capability.type, rel.capability.uniqueId, rel.capability.ownerId, rel.capability.name);
+ const isCapabilityFulfilled: boolean = rel.capability.isFulfilled();
+ if (isCapabilityFulfilled && toComponentInstanceCapability) {
+ // if capability is fulfilled and in component, then remove it
+ console.log('Capability is fulfilled', rel.capability.getFullTitle(), rel.capability.leftOccurrences);
+ toComponentInstance.capabilities[rel.capability.type].splice(
+ toComponentInstance.capabilities[rel.capability.type].findIndex((cap) => cap === toComponentInstanceCapability), 1
+ )
+ } else if (!isCapabilityFulfilled && !toComponentInstanceCapability) {
+ // if capability is unfulfilled and not in component, then add it
+ console.log('Capability is unfulfilled', rel.capability.getFullTitle(), rel.capability.leftOccurrences);
+ toComponentInstance.capabilities[rel.capability.type].push(rel.capability);
+ }
+ }
+ if (rel.requirement) {
+ const fromComponentInstance: ComponentInstance = componentInstances.find((inst) => inst.uniqueId === relation.fromNode);
+ const fromComponentInstanceRequirement: Requirement = fromComponentInstance.findRequirement(
+ rel.requirement.capability, rel.requirement.uniqueId, rel.requirement.ownerId, rel.requirement.name);
+ const isRequirementFulfilled: boolean = rel.requirement.isFulfilled();
+ if (isRequirementFulfilled && fromComponentInstanceRequirement) {
+ // if requirement is fulfilled and in component, then remove it
+ console.log('Requirement is fulfilled', rel.requirement.getFullTitle(), rel.requirement.leftOccurrences);
+ fromComponentInstance.requirements[rel.requirement.capability].splice(
+ fromComponentInstance.requirements[rel.requirement.capability].findIndex((req) => req === fromComponentInstanceRequirement), 1
+ )
+ } else if (!isRequirementFulfilled && !fromComponentInstanceRequirement) {
+ // if requirement is unfulfilled and not in component, then add it
+ console.log('Requirement is unfulfilled', rel.requirement.getFullTitle(), rel.requirement.leftOccurrences);
+ fromComponentInstance.requirements[rel.requirement.capability].push(rel.requirement);
+ }
+ }
+ });
+ }
+
+ public getRelationRequirementCapability(relationship: Relationship, sourceNode: ComponentInstance, targetNode: ComponentInstance): Promise<{ requirement: Requirement, capability: Capability }> {
+ // try find the requirement and capability in the source and target component instances:
+ let capability: Capability = targetNode.findCapability(undefined,
+ relationship.relation.capabilityUid,
+ relationship.relation.capabilityOwnerId,
+ relationship.relation.capability);
+ let requirement: Requirement = sourceNode.findRequirement(undefined,
+ relationship.relation.requirementUid,
+ relationship.relation.requirementOwnerId,
+ relationship.relation.requirement);
+
+ return new Promise<{ requirement: Requirement, capability: Capability }>((resolve, reject) => {
+ if (capability && requirement) {
+ resolve({capability, requirement});
+ }
+ else {
+ // if requirement and/or capability is missing, then fetch the full relation with its requirement and capability:
+ this.topologyTemplateService.fetchRelation(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, relationship.relation.id).subscribe((fetchedRelation) => {
+ this.syncComponentByRelation(fetchedRelation);
+ resolve({
+ capability: capability || fetchedRelation.relationships[0].capability,
+ requirement: requirement || fetchedRelation.relationships[0].requirement
+ });
+ }, reject);
+ }
+ });
+ }
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.spec.ts
new file mode 100644
index 0000000000..9dcc47f7cc
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.spec.ts
@@ -0,0 +1,158 @@
+import { TestBed } from '@angular/core/testing';
+import { SdcUiServices } from 'onap-ui-angular';
+import { Observable } from 'rxjs/Rx';
+import CollectionNodes = Cy.CollectionNodes;
+import { Mock } from 'ts-mockery';
+import { ComponentInstance } from '../../../../../models';
+import { ComponentMetadata } from '../../../../../models/component-metadata';
+import { Resource } from '../../../../../models/components/resource';
+import { CompositionCiNodeCp } from '../../../../../models/graph/nodes/composition-graph-nodes/composition-ci-node-cp';
+import { CompositionCiNodeVl } from '../../../../../models/graph/nodes/composition-graph-nodes/composition-ci-node-vl';
+import { EventListenerService } from '../../../../../services';
+import CollectionEdges = Cy.CollectionEdges;
+import { GRAPH_EVENTS } from '../../../../../utils/constants';
+import { ServiceServiceNg2 } from '../../../../services/component-services/service.service';
+import { TopologyTemplateService } from '../../../../services/component-services/topology-template.service';
+import { ComponentGenericResponse } from '../../../../services/responses/component-generic-response';
+import { QueueServiceUtils } from '../../../../utils/queue-service-utils';
+import { WorkspaceService } from '../../../workspace/workspace.service';
+import { CompositionService } from '../../composition.service';
+import { CommonGraphUtils } from '../common/common-graph-utils';
+import { CompositionGraphGeneralUtils } from './composition-graph-general-utils';
+import { CompositionGraphNodesUtils } from './composition-graph-nodes-utils';
+
+describe('composition graph nodes utils', () => {
+
+ const CP_TO_DELETE_ID = 'cp1';
+ const VL_TO_DELETE_ID = 'vl';
+ const CP2_ID = 'cp2';
+
+ let loaderServiceMock: Partial<SdcUiServices.LoaderService>;
+ let service: CompositionGraphNodesUtils;
+ let topologyServiceMock: TopologyTemplateService;
+ let queueServiceMock: QueueServiceUtils;
+ let workspaceServiceMock: WorkspaceService;
+ let compositionServiceMock: CompositionService;
+ let eventListenerServiceMock: EventListenerService;
+ const cpInstanceMock: ComponentInstance = Mock.of<ComponentInstance>({
+ uniqueId: CP_TO_DELETE_ID,
+ isVl: () => false
+ });
+ const vlInstanceMock: ComponentInstance = Mock.of<ComponentInstance>({
+ uniqueId: VL_TO_DELETE_ID,
+ isVl: () => true
+ });
+ const cp2InstanceMock: ComponentInstance = Mock.of<ComponentInstance>({
+ uniqueId: CP2_ID,
+ isVl: () => false
+ });
+
+ const cyMock = Mock.of<Cy.Instance>({
+ remove: jest.fn(),
+ collection: jest.fn()
+ });
+
+ const serviceServiceMock = Mock.of<ServiceServiceNg2>({
+ getComponentCompositionData : () => Observable.of(Mock.of<ComponentGenericResponse>())
+ });
+
+ // Instances on the graph cp, vl, cp2
+ const cp = Mock.from<CompositionCiNodeCp>({ id: CP_TO_DELETE_ID, componentInstance: cpInstanceMock });
+ const vl = Mock.from<CompositionCiNodeVl>({ id: VL_TO_DELETE_ID, componentInstance: vlInstanceMock });
+ const cp2 = Mock.from<CompositionCiNodeCp>({ id: CP2_ID, componentInstance: cp2InstanceMock });
+
+ beforeEach(() => {
+
+ loaderServiceMock = {
+ activate: jest.fn(),
+ deactivate: jest.fn()
+ };
+
+ topologyServiceMock = Mock.of<TopologyTemplateService>({
+ deleteComponentInstance : () => Observable.of(cpInstanceMock)
+ });
+
+ queueServiceMock = Mock.of<QueueServiceUtils>({
+ addBlockingUIAction : ( (f) => f() )
+ });
+
+ workspaceServiceMock = Mock.of<WorkspaceService>({
+ metadata: Mock.of<ComponentMetadata>( { uniqueId: 'topologyTemplateUniqueId' } )
+ });
+
+ compositionServiceMock = Mock.of<CompositionService>({
+ deleteComponentInstance : jest.fn()
+ });
+
+ eventListenerServiceMock = Mock.of<EventListenerService>({
+ notifyObservers : jest.fn()
+ });
+
+ TestBed.configureTestingModule({
+ imports: [],
+ providers: [
+ CompositionGraphNodesUtils,
+ {provide: WorkspaceService, useValue: workspaceServiceMock},
+ {provide: TopologyTemplateService, useValue: topologyServiceMock},
+ {provide: CompositionService, useValue: compositionServiceMock},
+ {provide: CompositionGraphGeneralUtils, useValue: {}},
+ {provide: CommonGraphUtils, useValue: {}},
+ {provide: EventListenerService, useValue: eventListenerServiceMock},
+ {provide: QueueServiceUtils, useValue: queueServiceMock},
+ {provide: ServiceServiceNg2, useValue: serviceServiceMock},
+ {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock}
+ ]
+ });
+ service = TestBed.get(CompositionGraphNodesUtils);
+ });
+
+ it('When a CP is deleted which is connected to a VL that has another leg to another CP, the VL is deleted as well', () => {
+ // Prepare a VL that is connected to both CP and CP2
+ const vlToDelete = Mock.of<CollectionNodes>({
+ data: () => vl,
+ connectedEdges: () => Mock.of<CollectionEdges>({
+ length: 2,
+ connectedNodes: () => [cp, cp2] as CollectionNodes
+ })
+ });
+
+ // Prepare a CP which is connected to a VL
+ const cpToDelete = Mock.of<CollectionNodes>({
+ data: () => cp,
+ connectedEdges: () => Mock.of<CollectionEdges>({
+ length: 1,
+ connectedNodes: () => [vlToDelete] as CollectionNodes
+ })
+ });
+ service.deleteNode(cyMock, Mock.of<Resource>(), cpToDelete);
+ expect(compositionServiceMock.deleteComponentInstance).toHaveBeenCalledWith(CP_TO_DELETE_ID);
+ expect(eventListenerServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, VL_TO_DELETE_ID);
+ expect(eventListenerServiceMock.notifyObservers).toHaveBeenLastCalledWith(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, CP_TO_DELETE_ID);
+ expect(cyMock.remove).toHaveBeenCalled();
+ });
+
+ it('When a CP is deleted which is solely connected to another VL the VL is not deleted', () => {
+ // Prepare a VL that is connected only to 1 CP
+ const vlToDelete = Mock.of<CollectionNodes>({
+ data: () => vl,
+ connectedEdges: () => Mock.of<CollectionEdges>({
+ length: 1,
+ connectedNodes: () => [cp] as CollectionNodes
+ })
+ });
+
+ // Prepare a CP which is connected to a VL
+ const cpToDelete = Mock.of<CollectionNodes>({
+ data: () => cp,
+ connectedEdges: () => Mock.of<CollectionEdges>({
+ length: 1,
+ connectedNodes: () => [vlToDelete] as CollectionNodes
+ })
+ });
+ service.deleteNode(cyMock, Mock.of<Resource>(), cpToDelete);
+ expect(compositionServiceMock.deleteComponentInstance).toHaveBeenCalledWith(CP_TO_DELETE_ID);
+ expect(eventListenerServiceMock.notifyObservers).toHaveBeenLastCalledWith(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, CP_TO_DELETE_ID);
+ expect(eventListenerServiceMock.notifyObservers).toHaveBeenCalledTimes(1);
+ expect(cyMock.remove).toHaveBeenCalled();
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.ts
new file mode 100644
index 0000000000..ea876c6d1a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.ts
@@ -0,0 +1,202 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+import { Injectable } from '@angular/core';
+import { Component as TopologyTemplate } from 'app/models';
+import {
+ ComponentInstance,
+ CompositionCiNodeVl, Service
+} from 'app/models';
+import { CompositionCiServicePathLink } from 'app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link';
+import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service';
+import { ServiceServiceNg2 } from 'app/ng2/services/component-services/service.service';
+import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service';
+import { ServiceGenericResponse } from 'app/ng2/services/responses/service-generic-response';
+import { QueueServiceUtils } from 'app/ng2/utils/queue-service-utils';
+import { EventListenerService } from 'app/services';
+import { GRAPH_EVENTS } from 'app/utils';
+import * as _ from 'lodash';
+import { SdcUiServices } from 'onap-ui-angular';
+import { CompositionService } from '../../composition.service';
+import { CommonGraphUtils } from '../common/common-graph-utils';
+import { CompositionGraphGeneralUtils } from './composition-graph-general-utils';
+
+/**
+ * Created by obarda on 11/9/2016.
+ */
+@Injectable()
+export class CompositionGraphNodesUtils {
+ constructor(private generalGraphUtils: CompositionGraphGeneralUtils,
+ private commonGraphUtils: CommonGraphUtils,
+ private eventListenerService: EventListenerService,
+ private queueServiceUtils: QueueServiceUtils,
+ private serviceService: ServiceServiceNg2,
+ private loaderService: SdcUiServices.LoaderService,
+ private compositionService: CompositionService,
+ private topologyTemplateService: TopologyTemplateService,
+ private workspaceService: WorkspaceService) {
+ }
+
+ /**
+ * 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();
+ });
+ }
+
+ public highlightMatchingNodesByName = (cy: Cy.Instance, nameToMatch: string) => {
+
+ cy.batch(() => {
+ cy.nodes("[name !@^= '" + nameToMatch + "']").style({'background-image-opacity': 0.4});
+ cy.nodes("[name @^= '" + nameToMatch + "']").style({'background-image-opacity': 1});
+ });
+
+ }
+
+ // Returns all nodes whose name starts with searchTerm
+ public getMatchingNodesByName = (cy: Cy.Instance, nameToMatch: string): Cy.CollectionNodes => {
+ return cy.nodes("[name @^= '" + nameToMatch + "']");
+ }
+
+ /**
+ * 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: TopologyTemplate, nodeToDelete: Cy.CollectionNodes): void {
+
+ this.loaderService.activate();
+ const onSuccess: (response: ComponentInstance) => void = (response: ComponentInstance) => {
+ // check whether the node is connected to any VLs that only have one other connection. If so, delete that VL as well
+ this.loaderService.deactivate();
+ this.compositionService.deleteComponentInstance(response.uniqueId);
+
+ const nodeToDeleteIsNotVl = nodeToDelete.data().componentInstance && !(nodeToDelete.data().componentInstance.isVl());
+ if (nodeToDeleteIsNotVl) {
+ const connectedVls: Cy.CollectionFirstNode[] = this.getConnectedVlToNode(nodeToDelete);
+ this.handleConnectedVlsToDelete(connectedVls);
+ }
+
+ // check whether there is a service path going through this node, and if so clean it from the graph.
+ const nodeId = nodeToDelete.data().id;
+ const connectedPathLinks = cy.collection(`[type="${CompositionCiServicePathLink.LINK_TYPE}"][source="${nodeId}"], [type="${CompositionCiServicePathLink.LINK_TYPE}"][target="${nodeId}"]`);
+ _.forEach(connectedPathLinks, (link, key) => {
+ cy.remove(`[pathId="${link.data().pathId}"]`);
+ });
+
+ // update service path list
+ this.serviceService.getComponentCompositionData(component).subscribe((serviceResponse: ServiceGenericResponse) => {
+ (component as Service).forwardingPaths = serviceResponse.forwardingPaths;
+ });
+
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, nodeId);
+
+ // update UI
+ cy.remove(nodeToDelete);
+ };
+
+ const onFailed: (response: any) => void = (response: any) => {
+ this.loaderService.deactivate();
+ };
+
+ this.queueServiceUtils.addBlockingUIAction(
+ () => {
+ const uniqueId = this.workspaceService.metadata.uniqueId;
+ const componentType = this.workspaceService.metadata.componentType;
+ this.topologyTemplateService.deleteComponentInstance(componentType, uniqueId, nodeToDelete.data().componentInstance.uniqueId).subscribe(onSuccess, onFailed);
+ }
+ );
+ }
+
+ /**
+ * Finds all VLs connected to a single node
+ * @param node
+ * @returns {Array<Cy.CollectionFirstNode>}
+ */
+ public getConnectedVlToNode = (node: Cy.CollectionNodes): Cy.CollectionFirstNode[] => {
+ const connectedVls: Cy.CollectionFirstNode[] = new Array<Cy.CollectionFirstNode>();
+ _.forEach(node.connectedEdges().connectedNodes(), (connectedNode: Cy.CollectionFirstNode) => {
+ const connectedNodeIsVl = connectedNode.data().componentInstance.isVl();
+ if (connectedNodeIsVl) {
+ connectedVls.push(connectedNode);
+ }
+ });
+ return connectedVls;
+ }
+
+ /**
+ * Delete all VLs that have only two connected nodes (this function is called when deleting a node)
+ * @param connectedVls
+ */
+ public handleConnectedVlsToDelete = (connectedVls: 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.uniqueId);
+ }
+ });
+ }
+
+ /**
+ * This function will update nodes position.
+ * @param cy
+ * @param component
+ * @param nodesMoved - the node/multiple nodes now moved by the user
+ */
+ public onNodesPositionChanged = (cy: Cy.Instance, component: TopologyTemplate, nodesMoved: Cy.CollectionNodes): void => {
+
+ if (nodesMoved.length === 0) {
+ return;
+ }
+
+ const isValidMove: boolean = this.generalGraphUtils.isGroupValidDrop(cy, nodesMoved);
+ if (isValidMove) {
+
+ const instancesToUpdate: ComponentInstance[] = new Array<ComponentInstance>();
+
+ _.each(nodesMoved, (node: Cy.CollectionFirstNode) => { // update all nodes new position
+
+ // update position
+ const newPosition: Cy.Position = this.commonGraphUtils.getNodePosition(node);
+ node.data().componentInstance.updatePosition(newPosition.x, newPosition.y);
+ instancesToUpdate.push(node.data().componentInstance);
+
+ });
+
+ if (instancesToUpdate.length > 0) {
+ this.generalGraphUtils.pushMultipleUpdateComponentInstancesRequestToQueue(instancesToUpdate);
+ }
+ } else {
+ // reset nodes position
+ nodesMoved.positions((i, node) => {
+ return {
+ x: +node.data().componentInstance.posX,
+ y: +node.data().componentInstance.posY
+ };
+ });
+ }
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-palette-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-palette-utils.ts
new file mode 100644
index 0000000000..1776c2f9b9
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-palette-utils.ts
@@ -0,0 +1,233 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+import {Injectable} from "@angular/core";
+import {CompositionGraphGeneralUtils, RequirementAndCapabilities} from "./composition-graph-general-utils";
+import {CommonGraphUtils} from "../common/common-graph-utils";
+import {EventListenerService} from "../../../../../services/event-listener-service";
+import {ResourceNamePipe} from "app/ng2/pipes/resource-name.pipe";
+import {ComponentInstanceFactory} from "app/utils/component-instance-factory";
+import {GRAPH_EVENTS, GraphUIObjects} from "app/utils/constants";
+import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service";
+import {DndDropEvent} from "ngx-drag-drop/ngx-drag-drop";
+import {SdcUiServices} from "onap-ui-angular"
+import { Component as TopologyTemplate, NodesFactory, CapabilitiesGroup, RequirementsGroup,
+ CompositionCiNodeBase, ComponentInstance, LeftPaletteComponent, Point } from "app/models";
+import {CompositionService} from "../../composition.service";
+import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service";
+import { QueueServiceUtils } from "app/ng2/utils/queue-service-utils";
+import {ComponentGenericResponse} from "../../../../services/responses/component-generic-response";
+import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils";
+import {CompositionGraphNodesUtils} from "./index";
+
+@Injectable()
+export class CompositionGraphPaletteUtils {
+
+ constructor(private generalGraphUtils:CompositionGraphGeneralUtils,
+ private nodesFactory:NodesFactory,
+ private commonGraphUtils:CommonGraphUtils,
+ private queueServiceUtils:QueueServiceUtils,
+ private eventListenerService:EventListenerService,
+ private topologyTemplateService: TopologyTemplateService,
+ private loaderService: SdcUiServices.LoaderService,
+ private compositionService: CompositionService,
+ private workspaceService: WorkspaceService,
+ private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils,
+ private nodesGraphUtils: CompositionGraphNodesUtils) {
+ }
+
+ /**
+ *
+ * @param Calculate matching nodes, highlight the matching nodes and fade the non matching nodes
+ * @param leftPaletteComponent
+ * @param _cy
+ * @returns void
+ * @private
+ */
+
+ public onComponentHoverIn = (leftPaletteComponent: LeftPaletteComponent, _cy: Cy.Instance) => {
+ const nodesData = this.nodesGraphUtils.getAllNodesData(_cy.nodes());
+ const nodesLinks = this.generalGraphUtils.getAllCompositionCiLinks(_cy);
+
+ if (this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(leftPaletteComponent.uniqueId)) {
+ const reqAndCap: RequirementAndCapabilities = this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(leftPaletteComponent.uniqueId);
+ const filteredNodesData = this.matchCapabilitiesRequirementsUtils.findMatchingNodesToComponentInstance(
+ { uniqueId: leftPaletteComponent.uniqueId, requirements: reqAndCap.requirements, capabilities: reqAndCap.capabilities} as ComponentInstance, nodesData, nodesLinks);
+
+ this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, _cy);
+ this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, _cy);
+ } else {
+
+ this.topologyTemplateService.getCapabilitiesAndRequirements(leftPaletteComponent.componentType, leftPaletteComponent.uniqueId).subscribe((response: ComponentGenericResponse) => {
+ let reqAndCap: RequirementAndCapabilities = {
+ capabilities: response.capabilities,
+ requirements: response.requirements
+ }
+ this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.setValue(leftPaletteComponent.uniqueId, reqAndCap);
+ });
+ }
+ }
+
+ /**
+ * 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:DragEvent, position?:Cy.Position, eventPosition?: Point) {
+ let bbox = <Cy.BoundingBox>{};
+ if (!position) {
+ position = event ? this.commonGraphUtils.getCytoscapeNodePosition(cy, event) : eventPosition;
+ }
+ 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:DragEvent) {
+
+ let componentInstanceToCreate:ComponentInstance = 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.deactivate();
+ };
+
+ //on success - update node data
+ let onSuccessCreatingInstance = (createInstance:ComponentInstance):void => {
+
+ this.loaderService.deactivate();
+ this.compositionService.addComponentInstance(createInstance);
+ createInstance.name = ResourceNamePipe.getDisplayName(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);
+ this.commonGraphUtils.addComponentInstanceNodeToGraph(cy, newNode);
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CREATE_COMPONENT_INSTANCE);
+ };
+
+ this.queueServiceUtils.addBlockingUIAction(() => {
+ let uniqueId = this.workspaceService.metadata.uniqueId;
+ let componentType = this.workspaceService.metadata.componentType;
+ this.topologyTemplateService.createComponentInstance(componentType, uniqueId, componentInstanceToCreate).subscribe(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) {
+ // let draggedElement = document.getElementById("draggable_element");
+ // // event.dataTransfer.setDragImage(draggableElement, 0, 0);
+ // if (event.clientX < GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET || event.clientY < GraphUIObjects.DIAGRAM_HEADER_OFFSET) { //hovering over palette. Dont bother computing validity of drop
+ // draggedElement.className = 'invalid-drag';
+ // event.dataTransfer.setDragImage(draggedElement.cloneNode(true), 0, 0);
+ // 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)) {
+ // draggedElement.className = 'valid-drag';
+ // event.dataTransfer.setDragImage(draggedElement.cloneNode(true), 0, 0);
+ // // event.dataTransfer.setDragImage(draggedElement, 0, 0);
+ // // event.dataTransfer.setDragImage(draggedElement, 0, 0);
+ //
+ // } else {
+ // draggedElement.className = 'invalid-drag';
+ // event.dataTransfer.setDragImage(draggedElement.cloneNode(true), 0, 0);
+ // }
+ // }
+
+ public isDragValid(cy:Cy.Instance, position: Point):boolean {
+ if (position.x < GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET || position.y < GraphUIObjects.DIAGRAM_HEADER_OFFSET) { //hovering over palette. Dont bother computing validity of drop
+ return false;
+ }
+
+ let offsetPosition = {
+ x: position.x - GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET,
+ y: position.y - GraphUIObjects.DIAGRAM_HEADER_OFFSET
+ };
+ let bbox = this._getNodeBBox(cy, null, offsetPosition, position);
+
+ if (this.generalGraphUtils.isPaletteDropValid(cy, bbox)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ /**
+ * 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 dragEvent
+ * @param component
+ */
+ public addNodeFromPalette(cy:Cy.Instance, dragEvent:DndDropEvent) {
+ this.loaderService.activate();
+
+ let draggedComponent:LeftPaletteComponent = dragEvent.data;
+
+ 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, dragEvent.event);
+
+ } else {
+
+ this.topologyTemplateService.getFullComponent(draggedComponent.componentType, draggedComponent.uniqueId).subscribe((topologyTemplate:TopologyTemplate) => {
+ draggedComponent.capabilities = topologyTemplate.capabilities;
+ draggedComponent.requirements = topologyTemplate.requirements;
+ this._createComponentInstanceOnGraphFromPaletteComponent(cy, draggedComponent, dragEvent.event);
+ });
+ }
+ }
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-service-path-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-service-path-utils.ts
new file mode 100644
index 0000000000..bc124fe9d1
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-service-path-utils.ts
@@ -0,0 +1,148 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+import * as _ from "lodash";
+import {CompositionGraphGeneralUtils} from "./composition-graph-general-utils";
+import {ServiceServiceNg2} from 'app/ng2/services/component-services/service.service';
+import {Service} from "app/models/components/service";
+import {ForwardingPath} from "app/models/forwarding-path";
+import {ForwardingPathLink} from "app/models/forwarding-path-link";
+import {ComponentRef, Injectable} from "@angular/core";
+import {CompositionCiServicePathLink} from "app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link";
+import {SdcUiServices} from "onap-ui-angular";
+import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils";
+import {ServicePathsListComponent} from "app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component";
+import {ButtonModel, ModalModel} from "app/models";
+import {ServicePathCreatorComponent} from "app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component";
+import {ModalService} from "app/ng2/services/modal.service";
+import {ModalComponent} from "app/ng2/components/ui/modal/modal.component";
+import {Select, Store} from "@ngxs/store";
+import {WorkspaceState} from "app/ng2/store/states/workspace.state";
+import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service";
+import {CompositionService} from "../../composition.service";
+import {CommonGraphUtils} from "../common/common-graph-utils";
+import {GRAPH_EVENTS} from "app/utils/constants";
+import {EventListenerService} from "app/services/event-listener-service";
+
+@Injectable()
+export class ServicePathGraphUtils {
+
+ constructor(
+ private generalGraphUtils: CompositionGraphGeneralUtils,
+ private serviceService: ServiceServiceNg2,
+ private commonGraphUtils: CommonGraphUtils,
+ private loaderService: SdcUiServices.LoaderService,
+ private queueServiceUtils: QueueServiceUtils,
+ private modalService: ModalService,
+ private workspaceService: WorkspaceService,
+ private compositionService: CompositionService,
+ private store:Store,
+ private eventListenerService: EventListenerService
+ ) {
+ }
+
+ private isViewOnly = (): boolean => {
+ return this.store.selectSnapshot(state => state.workspace.isViewOnly);
+ }
+ private modalInstance: ComponentRef<ModalComponent>;
+
+ public deletePathsFromGraph(cy: Cy.Instance) {
+ cy.remove(`[type="${CompositionCiServicePathLink.LINK_TYPE}"]`);
+ }
+
+ public drawPath(cy: Cy.Instance, forwardingPath: ForwardingPath) {
+ let pathElements = forwardingPath.pathElements.listToscaDataDefinition;
+
+ _.forEach(pathElements, (link: ForwardingPathLink) => {
+ let data: CompositionCiServicePathLink = new CompositionCiServicePathLink(link);
+ data.source = _.find(
+ this.compositionService.componentInstances,
+ instance => instance.name === data.forwardingPathLink.fromNode
+ ).uniqueId;
+ data.target = _.find(
+ this.compositionService.componentInstances,
+ instance => instance.name === data.forwardingPathLink.toNode
+ ).uniqueId;
+ data.pathId = forwardingPath.uniqueId;
+ data.pathName = forwardingPath.name;
+ this.commonGraphUtils.insertServicePathLinkToGraph(cy, data);
+ });
+ }
+
+ public createOrUpdateServicePath = (path: any): void => {
+ this.loaderService.activate();
+
+ let onSuccess: (response: ForwardingPath) => void = (response: ForwardingPath) => {
+ this.loaderService.deactivate();
+ this.compositionService.forwardingPaths[response.uniqueId] = response;
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_SERVICE_PATH_CREATED, response.uniqueId)
+ };
+
+ this.queueServiceUtils.addBlockingUIAction(
+ () => this.serviceService.createOrUpdateServicePath(this.workspaceService.metadata.uniqueId, path).subscribe(onSuccess
+ , (error) => {this.loaderService.deactivate()})
+ );
+ };
+
+ public onCreateServicePath = (): void => {
+ // this.showServicePathMenu = false;
+ let cancelButton: ButtonModel = new ButtonModel('Cancel', 'outline white', this.modalService.closeCurrentModal);
+ let saveButton: ButtonModel = new ButtonModel('Create', 'blue', this.createPath, this.getDisabled);
+ let modalModel: ModalModel = new ModalModel('l', 'Create Service Flow', '', [saveButton, cancelButton], 'standard', true);
+ this.modalInstance = this.modalService.createCustomModal(modalModel);
+ this.modalService.addDynamicContentToModal(this.modalInstance, ServicePathCreatorComponent, {serviceId: this.workspaceService.metadata.uniqueId});
+ this.modalInstance.instance.open();
+ };
+
+ public onListServicePath = (): void => {
+ // this.showServicePathMenu = false;
+ let cancelButton: ButtonModel = new ButtonModel('Close', 'outline white', this.modalService.closeCurrentModal);
+ let modalModel: ModalModel = new ModalModel('md', 'Service Flows List', '', [cancelButton], 'standard', true);
+ this.modalInstance = this.modalService.createCustomModal(modalModel);
+ this.modalService.addDynamicContentToModal(this.modalInstance, ServicePathsListComponent, {
+ serviceId: this.workspaceService.metadata.uniqueId,
+ onCreateServicePath: this.onCreateServicePath,
+ onEditServicePath: this.onEditServicePath,
+ isViewOnly: this.isViewOnly()
+ });
+ this.modalInstance.instance.open();
+ };
+
+ public onEditServicePath = (id: string): void => {
+ let cancelButton: ButtonModel = new ButtonModel('Cancel', 'outline white', this.modalService.closeCurrentModal);
+ let saveButton: ButtonModel = new ButtonModel('Save', 'blue', this.createPath, this.getDisabled);
+ let modalModel: ModalModel = new ModalModel('l', 'Edit Path', '', [saveButton, cancelButton], 'standard', true);
+ this.modalInstance = this.modalService.createCustomModal(modalModel);
+ this.modalService.addDynamicContentToModal(this.modalInstance, ServicePathCreatorComponent, {
+ serviceId: this.workspaceService.metadata.uniqueId,
+ pathId: id
+ });
+ this.modalInstance.instance.open();
+ };
+
+ public getDisabled = (): boolean => {
+ return this.isViewOnly() || !this.modalInstance.instance.dynamicContent.instance.checkFormValidForSubmit();
+ };
+
+ public createPath = (): void => {
+ this.createOrUpdateServicePath(this.modalInstance.instance.dynamicContent.instance.createServicePathData());
+ this.modalService.closeCurrentModal();
+ };
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-zone-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-zone-utils.ts
new file mode 100644
index 0000000000..9e97ec0f00
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-zone-utils.ts
@@ -0,0 +1,204 @@
+import {
+ Point,
+ PolicyInstance,
+ Zone,
+ LeftPaletteMetadataTypes,
+ ZoneInstance,
+ ZoneInstanceType,
+ ZoneInstanceAssignmentType
+} from "app/models";
+import {CanvasHandleTypes} from "app/utils";
+import {Observable} from "rxjs";
+import {GroupInstance} from "app/models/graph/zones/group-instance";
+import {Injectable} from "@angular/core";
+import {DynamicComponentService} from "app/ng2/services/dynamic-component.service";
+import {PoliciesService} from "app/ng2/services/policies.service";
+import {GroupsService} from "app/ng2/services/groups.service";
+import {Store} from "@ngxs/store";
+import {CompositionService} from "../../composition.service";
+import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service";
+import { PaletteAnimationComponent } from "app/ng2/pages/composition/palette/palette-animation/palette-animation.component";
+
+@Injectable()
+export class CompositionGraphZoneUtils {
+
+ constructor(private dynamicComponentService: DynamicComponentService,
+ private policiesService: PoliciesService,
+ private groupsService: GroupsService,
+ private workspaceService: WorkspaceService,
+ private compositionService: CompositionService) {
+ }
+
+
+ public createCompositionZones = (): Array<Zone> => {
+ let zones: Array<Zone> = [];
+
+ zones[ZoneInstanceType.POLICY] = new Zone('Policies', 'P', ZoneInstanceType.POLICY);
+ zones[ZoneInstanceType.GROUP] = new Zone('Groups', 'G', ZoneInstanceType.GROUP);
+
+ return zones;
+ }
+
+ public showZone = (zone: Zone): void => {
+ zone.visible = true;
+ zone.minimized = false;
+ }
+
+ public getZoneTypeForPaletteComponent = (componentCategory: LeftPaletteMetadataTypes) => {
+ if (componentCategory == LeftPaletteMetadataTypes.Group) {
+ return ZoneInstanceType.GROUP;
+ } else if (componentCategory == LeftPaletteMetadataTypes.Policy) {
+ return ZoneInstanceType.POLICY;
+ }
+ };
+
+ public initZoneInstances(zones: Array<Zone>) {
+
+ if (this.compositionService.groupInstances && this.compositionService.groupInstances.length) {
+ this.showZone(zones[ZoneInstanceType.GROUP]);
+ zones[ZoneInstanceType.GROUP].instances = [];
+ _.forEach(this.compositionService.groupInstances, (group: GroupInstance) => {
+ let newInstance = new ZoneInstance(group, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId);
+ this.addInstanceToZone(zones[ZoneInstanceType.GROUP], newInstance);
+ });
+ }
+
+ if (this.compositionService.policies && this.compositionService.policies.length) {
+ this.showZone(zones[ZoneInstanceType.POLICY]);
+ zones[ZoneInstanceType.POLICY].instances = [];
+ _.forEach(this.compositionService.policies, (policy: PolicyInstance) => {
+ let newInstance = new ZoneInstance(policy, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId);
+ this.addInstanceToZone(zones[ZoneInstanceType.POLICY], newInstance);
+
+ });
+ }
+ }
+
+ public findAndUpdateZoneInstanceData(zones: Array<Zone>, instanceData: PolicyInstance | GroupInstance) {
+ _.forEach(zones, (zone: Zone) => {
+ _.forEach(zone.instances, (zoneInstance: ZoneInstance) => {
+ if (zoneInstance.instanceData.uniqueId === instanceData.uniqueId) {
+ zoneInstance.updateInstanceData(instanceData);
+ }
+ });
+ });
+ }
+
+ public updateTargetsOrMembersOnCanvasDelete = (canvasNodeID: string, zones: Array<Zone>, type: ZoneInstanceAssignmentType): void => {
+ _.forEach(zones, (zone) => {
+ _.forEach(zone.instances, (zoneInstance: ZoneInstance) => {
+ if (zoneInstance.isAlreadyAssigned(canvasNodeID)) {
+ zoneInstance.addOrRemoveAssignment(canvasNodeID, type);
+ //remove it from our list of BE targets and members as well (so that it will not be sent in future calls to BE).
+ zoneInstance.instanceData.setSavedAssignments(zoneInstance.assignments);
+ }
+ });
+ });
+ };
+
+ public createZoneInstanceFromLeftPalette = (zoneType: ZoneInstanceType, paletteComponentType: string): Observable<ZoneInstance> => {
+
+ if (zoneType === ZoneInstanceType.POLICY) {
+ return this.policiesService.createPolicyInstance(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, paletteComponentType).map(response => {
+ let newInstance = new PolicyInstance(response);
+ this.compositionService.addPolicyInstance(newInstance);
+ return new ZoneInstance(newInstance, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId);
+ });
+ } else if (zoneType === ZoneInstanceType.GROUP) {
+ return this.groupsService.createGroupInstance(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, paletteComponentType).map(response => {
+ let newInstance = new GroupInstance(response);
+ this.compositionService.addGroupInstance(newInstance);
+ return new ZoneInstance(newInstance, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId);
+ });
+ }
+ }
+
+ public addInstanceToZone(zone: Zone, instance: ZoneInstance, hide?: boolean) {
+ if (hide) {
+ instance.hidden = true;
+ }
+ zone.instances.push(instance);
+
+ };
+
+ private findZoneCoordinates(zoneType): Point {
+ let point: Point = new Point(0, 0);
+ let zone = angular.element(document.querySelector('.' + zoneType + '-zone'));
+ let wrapperZone = zone.offsetParent();
+ point.x = zone.prop('offsetLeft') + wrapperZone.prop('offsetLeft');
+ point.y = zone.prop('offsetTop') + wrapperZone.prop('offsetTop');
+ return point;
+ }
+
+ public createPaletteToZoneAnimation = (startPoint: Point, zoneType: ZoneInstanceType, newInstance: ZoneInstance) => {
+ let zoneTypeName = ZoneInstanceType[zoneType].toLowerCase();
+ let paletteToZoneAnimation = this.dynamicComponentService.createDynamicComponent(PaletteAnimationComponent);
+ paletteToZoneAnimation.instance.from = startPoint;
+ paletteToZoneAnimation.instance.type = zoneType;
+ paletteToZoneAnimation.instance.to = this.findZoneCoordinates(zoneTypeName);
+ paletteToZoneAnimation.instance.zoneInstance = newInstance;
+ paletteToZoneAnimation.instance.iconName = zoneTypeName;
+ paletteToZoneAnimation.instance.runAnimation();
+ }
+
+ public startCyTagMode = (cy: Cy.Instance) => {
+ cy.autolock(true);
+ cy.nodes().unselectify();
+ cy.emit('tagstart'); //dont need to show handles because they're already visible bcz of hover event
+
+ };
+
+ public endCyTagMode = (cy: Cy.Instance) => {
+ cy.emit('tagend');
+ cy.nodes().selectify();
+ cy.autolock(false);
+ };
+
+ public handleTagClick = (cy: Cy.Instance, zoneInstance: ZoneInstance, nodeId: string) => {
+ zoneInstance.addOrRemoveAssignment(nodeId, ZoneInstanceAssignmentType.COMPONENT_INSTANCES);
+ this.showZoneTagIndicationForNode(nodeId, zoneInstance, cy);
+ };
+
+ public showGroupZoneIndications = (groupInstances: Array<ZoneInstance>, policyInstance: ZoneInstance) => {
+ groupInstances.forEach((groupInstance: ZoneInstance) => {
+ let handle: string = this.getCorrectHandleForNode(groupInstance.instanceData.uniqueId, policyInstance);
+ groupInstance.showHandle(handle);
+ })
+ };
+
+ public hideGroupZoneIndications = (instances: Array<ZoneInstance>) => {
+ instances.forEach((instance) => {
+ instance.hideHandle();
+ })
+ }
+
+ public showZoneTagIndications = (cy: Cy.Instance, zoneInstance: ZoneInstance) => {
+
+ cy.nodes().forEach(node => {
+ let handleType: string = this.getCorrectHandleForNode(node.id(), zoneInstance);
+ cy.emit('showhandle', [node, handleType]);
+ });
+ };
+
+ public showZoneTagIndicationForNode = (nodeId: string, zoneInstance: ZoneInstance, cy: Cy.Instance) => {
+ let node = cy.getElementById(nodeId);
+ let handleType: string = this.getCorrectHandleForNode(nodeId, zoneInstance);
+ cy.emit('showhandle', [node, handleType]);
+ }
+
+ public hideZoneTagIndications = (cy: Cy.Instance) => {
+ cy.emit('hidehandles');
+ };
+
+ public getCorrectHandleForNode = (nodeId: string, zoneInstance: ZoneInstance): string => {
+ if (zoneInstance.isAlreadyAssigned(nodeId)) {
+ if (zoneInstance.type == ZoneInstanceType.POLICY) {
+ return CanvasHandleTypes.TAGGED_POLICY;
+ } else {
+ return CanvasHandleTypes.TAGGED_GROUP;
+ }
+ } else {
+ return CanvasHandleTypes.TAG_AVAILABLE;
+ }
+ };
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/index.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/index.ts
new file mode 100644
index 0000000000..e7f11af248
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/index.ts
@@ -0,0 +1,29 @@
+/**
+ * Created by ob0695 on 6/3/2018.
+ */
+// export * from './composition-graph-general-utils';
+// export * from './composition-graph-links-utils';
+// export * from './composition-graph-nodes-utils';
+// export * from './composition-graph-palette-utils';
+// export * from './composition-graph-service-path-utils';
+// export * from './composition-graph-zone-utils';
+
+
+import {CompositionGraphGeneralUtils} from './composition-graph-general-utils';
+import {CompositionGraphNodesUtils} from './composition-graph-nodes-utils';
+import {MatchCapabilitiesRequirementsUtils} from './match-capability-requierment-utils'
+import {CompositionGraphPaletteUtils} from './composition-graph-palette-utils';
+import {CompositionGraphZoneUtils} from './composition-graph-zone-utils';
+import {ServicePathGraphUtils} from './composition-graph-service-path-utils';
+import {CompositionGraphLinkUtils} from "./composition-graph-links-utils";
+
+
+export {
+ CompositionGraphGeneralUtils,
+ CompositionGraphLinkUtils,
+ CompositionGraphNodesUtils,
+ MatchCapabilitiesRequirementsUtils,
+ CompositionGraphPaletteUtils,
+ CompositionGraphZoneUtils,
+ ServicePathGraphUtils
+}; \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.spec.ts
new file mode 100644
index 0000000000..dbfc3e7219
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.spec.ts
@@ -0,0 +1,342 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Mock } from 'ts-mockery';
+import {
+ CapabilitiesGroup,
+ Capability, ComponentInstance, CompositionCiLinkBase, CompositionCiNodeBase, CompositionCiNodeCp,
+ CompositionCiNodeVf, CompositionCiNodeVl,
+ Requirement, RequirementsGroup
+} from '../../../../../models';
+import { MatchCapabilitiesRequirementsUtils } from './match-capability-requierment-utils';
+
+describe('match capability requirements utils service ', () => {
+
+ const bindableReq = Mock.of<Requirement>({
+ capability : 'tosca.capabilities.network.Bindable',
+ name: 'virtualBinding',
+ relationship: 'tosca.relationships.network.BindsTo',
+ uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.virtualBinding',
+ ownerId : 'extcp0',
+ ownerName : 's'
+ });
+
+ const virtualLinkReq = Mock.of<Requirement>({
+ capability: 'tosca.capabilities.network.Linkable',
+ name: 'virtualLink',
+ relationship: 'tosca.relationships.network.LinksTo',
+ uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.virtualLink',
+ ownerId : '',
+ ownerName : 's'
+ });
+
+ const storeAttachmentReq = Mock.of<Requirement>({
+ capability: 'tosca.capabilities.Attachment',
+ name: 'local_storage',
+ relationship: 'tosca.relationships.AttachesTo',
+ uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.local_storage',
+ node: 'tosca.nodes.BlockStorage',
+ ownerId : '',
+ ownerName : 's'
+ });
+
+ const vlAttachmentReq = Mock.of<Requirement>({
+ capability: 'tosca.capabilities.Attachment',
+ name: 'local_storage',
+ relationship: 'tosca.relationships.AttachesTo',
+ uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.local_storage',
+ node: 'tosca.nodes.BlockStorage',
+ ownerId : '',
+ ownerName : 's'
+ });
+
+ const extVirtualLinkReq = Mock.of<Requirement>({
+ capability: 'tosca.capabilities.network.Linkable',
+ name: 'external_virtualLink',
+ relationship: 'tosca.relationships.network.LinksTo',
+ uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.external_virtualLink'
+ });
+
+ const dependencyReq = Mock.of<Requirement>({
+ capability: 'tosca.capabilities.Node',
+ name: 'dependency',
+ relationship: 'tosca.relationships.DependsOn',
+ uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.dependency'
+ });
+
+ const featureCap = Mock.of<Capability>({
+ type: 'tosca.capabilities.Node',
+ name: 'feature',
+ uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.feature',
+ maxOccurrences: 'UNBOUNDED',
+ minOccurrences: '1'
+ });
+
+ const internalConnPointCap = Mock.of<Capability>({
+ type: 'tosca.capabilities.Node',
+ name: 'internal_connectionPoint',
+ capabilitySources : ['org.openecomp.resource.cp.extCP'],
+ uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.internal_connectionPoint',
+ maxOccurrences: 'UNBOUNDED',
+ minOccurrences: '1'
+ });
+
+ const blockStoreAttachmentCap = Mock.of<Capability>({
+ type: 'tosca.capabilities.Attachment',
+ name: 'attachment',
+ capabilitySources: ['tosca.nodes.BlockStorage'],
+ uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.attachment',
+ maxOccurrences: 'UNBOUNDED',
+ minOccurrences: '1'
+ });
+
+ const bindingCap = Mock.of<Capability>({
+ type: 'tosca.capabilities.network.Bindable',
+ name: 'binding',
+ capabilitySources: ['tosca.nodes.Compute'],
+ uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.binding',
+ maxOccurrences: 'UNBOUNDED',
+ minOccurrences: '1',
+ });
+
+ const linkableCap = Mock.of<Capability>({
+ type: 'tosca.capabilities.network.Linkable',
+ capabilitySources: ['org.openecomp.resource.vl.extVL'],
+ uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.virtual_linkable',
+ maxOccurrences: 'UNBOUNDED',
+ minOccurrences: '1'
+ });
+
+ const nodeCompute = Mock.of<CompositionCiNodeVf>({
+ name: 'Compute 0',
+ componentInstance: Mock.of<ComponentInstance>({
+ componentName: 'Compute',
+ uniqueId : 'compute0',
+ requirements: Mock.of<RequirementsGroup>({
+ 'tosca.capabilities.Node' : [ dependencyReq ],
+ 'tosca.capabilities.Attachment' : [ storeAttachmentReq ]
+ }),
+ capabilities: Mock.of<CapabilitiesGroup>({
+ 'tosca.capabilities.network.Bindable' : [ bindingCap ],
+ 'tosca.capabilities.Node' : [ featureCap ]
+ })
+ })
+ });
+
+ const nodeBlockStorage = Mock.of<CompositionCiNodeVf>({
+ name: 'BlockStorage 0',
+ componentInstance: Mock.of<ComponentInstance>({
+ componentName: 'BlockStorage',
+ uniqueId : 'blockstorage0',
+ requirements: Mock.of<RequirementsGroup>({
+ 'tosca.capabilities.Node' : [ dependencyReq ]
+ }),
+ capabilities: Mock.of<CapabilitiesGroup>({
+ 'tosca.capabilities.Attachment' : [ blockStoreAttachmentCap ],
+ 'tosca.capabilities.Node' : [ featureCap ]
+ })
+ })
+ });
+
+ const nodeVl = Mock.of<CompositionCiNodeVl>({
+ name: 'ExtVL 0',
+ componentInstance: Mock.of<ComponentInstance>({
+ componentName: 'BlockStorage',
+ uniqueId : 'extvl0',
+ requirements: Mock.of<RequirementsGroup>({
+ 'tosca.capabilities.Node' : [ dependencyReq ]
+ }),
+ capabilities: Mock.of<CapabilitiesGroup>({
+ 'tosca.capabilities.network.Linkable' : [ linkableCap ],
+ 'tosca.capabilities.Node' : [ featureCap ]
+ })
+ })
+ });
+
+ const nodeCp = Mock.of<CompositionCiNodeCp>({
+ name: 'ExtCP 0',
+ componentInstance: Mock.of<ComponentInstance>({
+ componentName: 'ExtCP',
+ uniqueId : 'extcp0',
+ requirements: Mock.of<RequirementsGroup>({
+ 'tosca.capabilities.network.Linkable' : [ virtualLinkReq ],
+ 'tosca.capabilities.network.Bindable' : [ bindableReq ]
+ }),
+ capabilities: Mock.of<CapabilitiesGroup>({
+ 'tosca.capabilities.Node' : [ featureCap ]
+ })
+ })
+ });
+
+ let service: MatchCapabilitiesRequirementsUtils;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [],
+ providers: [MatchCapabilitiesRequirementsUtils]
+ });
+
+ service = TestBed.get(MatchCapabilitiesRequirementsUtils);
+ });
+
+ it('match capability requirements utils should be defined', () => {
+ console.log(JSON.stringify(service));
+ expect(service).toBeDefined();
+ });
+
+ describe('isMatch function ', () => {
+
+ it('capability type not equal to requirement capability, match is false', () => {
+ const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable11'});
+ const capability = Mock.of<Capability>({type: 'tosca.capabilities.network.Linkable'});
+ expect(service.isMatch(requirement, capability)).toBeFalsy();
+ });
+
+ it('capability type equal to requirement capability and requirement node not exist, match is true', () => {
+ const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable'});
+ const capability = Mock.of<Capability>({type: 'tosca.capabilities.network.Linkable'});
+ expect(service.isMatch(requirement, capability)).toBeTruthy();
+ });
+
+ it('is match - capability type equal to requirement capability and requirement node exist and includes in capability sources, match is true', () => {
+ const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable', node: 'node1'});
+ const capability = Mock.of<Capability>({
+ type: 'tosca.capabilities.network.Linkable',
+ capabilitySources: ['node1', 'node2', 'node3']
+ });
+ expect(service.isMatch(requirement, capability)).toBeTruthy();
+ });
+
+ it('no match - capability type equal to requirement capability and requirement node but not includes in capability sources, match is false', () => {
+ const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable', node: 'node4'});
+ const capability = Mock.of<Capability>({
+ type: 'tosca.capabilities.network.Linkable',
+ capabilitySources: ['node1', 'node2', 'node3']
+ });
+ expect(service.isMatch(requirement, capability)).toBeFalsy();
+ });
+ });
+
+ describe('hasUnfulfilledRequirementContainingMatch function ', () => {
+
+ it('node have no componentInstance, return false', () => {
+ const node = Mock.of<CompositionCiNodeVf>({componentInstance: undefined});
+ expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeFalsy();
+ });
+
+ it('node have componentInstance data but no unfulfilled requirements, return false', () => {
+ const node = Mock.of<CompositionCiNodeVf>({componentInstance: Mock.of<ComponentInstance>()});
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([]);
+ expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeFalsy();
+ });
+
+ it('node have componentInstance data and unfulfilled requirements but no match found, return false', () => {
+ const node = Mock.of<CompositionCiNodeVf>({componentInstance: Mock.of<ComponentInstance>()});
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]);
+ jest.spyOn(service, 'containsMatch').mockReturnValue(false);
+ expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeFalsy();
+ });
+
+ it('node have componentInstance data with unfulfilled requirements and match found, return true', () => {
+ const node = Mock.of<CompositionCiNodeVf>({componentInstance: Mock.of<ComponentInstance>()});
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]);
+ jest.spyOn(service, 'containsMatch').mockReturnValue(true);
+ expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeTruthy();
+ });
+ });
+
+ describe('getMatches function ', () => {
+ let fromId: string;
+ let toId: string;
+
+ beforeEach(() => {
+ fromId = 'from_id';
+ toId = 'to_id';
+ });
+
+ it('node have no unfulfilled requirements, return empty match array', () => {
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([]);
+ expect(service.getMatches({}, {}, [], fromId, toId, true)).toHaveLength(0);
+ });
+
+ it('node have unfulfilled requirements but no capabilities, return empty match array', () => {
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]);
+ expect(service.getMatches({}, {}, [], fromId, toId, true)).toHaveLength(0);
+ });
+
+ it('node have unfulfilled requirements and capabilities but no match found, return empty match array', () => {
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]);
+ jest.spyOn(service, 'isMatch').mockReturnValue(false);
+ expect(service.getMatches({}, {}, [], fromId, toId, true)).toHaveLength(0);
+ });
+
+ it('node have 2 unfulfilled requirements and 2 capabilities and match found, return 4 matches', () => {
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]);
+ const capabilities = {aaa: Mock.of<Capability>(), bbb: Mock.of<Capability>()};
+ jest.spyOn(service, 'isMatch').mockReturnValue(true);
+ expect(service.getMatches({}, capabilities, [], fromId, toId, true)).toHaveLength(4);
+ });
+ });
+
+ describe('Find matching nodes ===>', () => {
+
+ it('should find matching nodes with component instance', () => {
+ const nodes = [ nodeBlockStorage, nodeCompute, nodeVl ];
+ let matchingNodes: any;
+
+ // Compute can connect to Block Store
+ matchingNodes = service.findMatchingNodesToComponentInstance(nodeCompute.componentInstance, nodes, []);
+ expect(matchingNodes).toHaveLength(1);
+ expect(matchingNodes).toContain(nodeBlockStorage);
+
+ // Block Storage can connect to Compute
+ matchingNodes = service.findMatchingNodesToComponentInstance(nodeBlockStorage.componentInstance, nodes, []);
+ expect(matchingNodes).toHaveLength(1);
+ expect(matchingNodes).toContain(nodeCompute);
+
+ // Vl has no matches
+ matchingNodes = service.findMatchingNodesToComponentInstance(nodeVl.componentInstance, nodes, []);
+ expect(matchingNodes).toHaveLength(0);
+
+ // CP should be able to connect to VL and Compute
+ matchingNodes = service.findMatchingNodesToComponentInstance(nodeCp.componentInstance, nodes, []);
+ expect(matchingNodes).toHaveLength(2);
+ expect(matchingNodes).toContain(nodeCompute);
+ expect(matchingNodes).toContain(nodeVl);
+ });
+
+ it('try with empty list of nodes', () => {
+ const nodes = [ ];
+ let matchingNodes: any;
+
+ // Compute can connect to Block Store
+ matchingNodes = service.findMatchingNodesToComponentInstance(nodeCompute.componentInstance, nodes, []);
+ expect(matchingNodes).toHaveLength(0);
+ });
+
+ it('should detect fulfilled connection with compute node', () => {
+ const nodes = [ nodeBlockStorage, nodeCompute, nodeVl ];
+ let matchingNodes: any;
+ const link = {
+ relation: {
+ fromNode: 'extcp0',
+ toNode: 'compute0',
+ relationships: [{
+ relation: {
+ requirementOwnerId: 'extcp0',
+ requirement: 'virtualBinding',
+ relationship: {
+ type: 'tosca.relationships.network.BindsTo'
+ }
+
+ }
+ }]
+ }
+ };
+
+ const links = [link];
+ // CP should be able to connect to VL only since it already has a link with compute
+ matchingNodes = service.findMatchingNodesToComponentInstance(nodeCp.componentInstance, nodes, links as CompositionCiLinkBase[]);
+ expect(matchingNodes).toHaveLength(1);
+ expect(matchingNodes).toContain(nodeVl);
+ });
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.ts
new file mode 100644
index 0000000000..c3a1286a97
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.ts
@@ -0,0 +1,196 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+import { Injectable } from '@angular/core';
+import {
+ CapabilitiesGroup, Capability, ComponentInstance, CompositionCiLinkBase,
+ CompositionCiNodeBase, Match, Requirement, RequirementsGroup
+} from 'app/models';
+import * as _ from 'lodash';
+
+/**
+ * Created by obarda on 1/1/2017.
+ */
+@Injectable()
+export class MatchCapabilitiesRequirementsUtils {
+
+ /**
+ * Shows + icon in corner of each node passed in
+ * @param filteredNodesData
+ * @param cy
+ */
+ public highlightMatchingComponents(filteredNodesData, cy: Cy.Instance) {
+ _.each(filteredNodesData, (data: any) => {
+ const 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?) {
+ const 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});
+ });
+ }
+
+ public getMatchedRequirementsCapabilities(fromComponentInstance: ComponentInstance,
+ toComponentInstance: ComponentInstance,
+ links: CompositionCiLinkBase[]): Match[] {
+ const fromToMatches: Match[] = this.getMatches(fromComponentInstance.requirements,
+ toComponentInstance.capabilities,
+ links,
+ fromComponentInstance.uniqueId,
+ toComponentInstance.uniqueId, true);
+ const toFromMatches: Match[] = this.getMatches(toComponentInstance.requirements,
+ fromComponentInstance.capabilities,
+ links,
+ toComponentInstance.uniqueId,
+ fromComponentInstance.uniqueId, false);
+
+ return fromToMatches.concat(toFromMatches);
+ }
+
+ /***** REFACTORED FUNCTIONS START HERE *****/
+
+ public getMatches(requirements: RequirementsGroup, capabilities: CapabilitiesGroup, links: CompositionCiLinkBase[],
+ fromId: string, toId: string, isFromTo: boolean): Match[] {
+ const matches: Match[] = [];
+ const unfulfilledReqs = this.getUnfulfilledRequirements(fromId, requirements, links);
+ _.forEach(unfulfilledReqs, (req) => {
+ _.forEach(_.flatten(_.values(capabilities)), (capability: Capability) => {
+ if (this.isMatch(req, capability)) {
+ if (isFromTo) {
+ matches.push(new Match(req, capability, isFromTo, fromId, toId));
+ } else {
+ matches.push(new Match(req, capability, isFromTo, toId, fromId));
+ }
+ }
+ });
+ });
+ return matches;
+ }
+
+ public getUnfulfilledRequirements = (fromNodeId: string, requirements: RequirementsGroup, links: CompositionCiLinkBase[]): Requirement[] => {
+ const requirementArray: Requirement[] = [];
+ _.forEach(_.flatten(_.values(requirements)), (requirement: Requirement) => {
+ const reqFulfilled = this.isRequirementFulfilled(fromNodeId, requirement, links);
+ if (requirement.name !== 'dependency' && requirement.parentName !== 'dependency' && !reqFulfilled) {
+ requirementArray.push(requirement);
+ }
+ });
+ return requirementArray;
+ }
+
+ /**
+ * Returns true if there is a match between the capabilities and requirements that are passed in
+ * @param requirements
+ * @param capabilities
+ * @returns {boolean}
+ */
+ public containsMatch = (requirements: Requirement[], capabilities: CapabilitiesGroup): boolean => {
+ return _.some(requirements, (req: Requirement) => {
+ return _.some(_.flatten(_.values(capabilities)), (capability: Capability) => {
+ return this.isMatch(req, capability);
+ });
+ });
+ }
+
+ public hasUnfulfilledRequirementContainingMatch = (node: CompositionCiNodeBase, componentRequirements: Requirement[], capabilities: CapabilitiesGroup, links: CompositionCiLinkBase[]) => {
+ if (node && node.componentInstance) {
+ // Check if node has unfulfilled requirement that can be filled by component (#2)
+ const nodeRequirements: Requirement[] = this.getUnfulfilledRequirements(node.componentInstance.uniqueId, node.componentInstance.requirements, links);
+ if (!nodeRequirements.length) {
+ return false;
+ }
+ if (this.containsMatch(nodeRequirements, capabilities)) {
+ return true;
+ }
+ }
+ }
+
+ /**
+ * Returns array of nodes that can connect to the component.
+ * In order to connect, one of the following conditions must be met:
+ * 1. component has an unfulfilled requirement that matches a node's capabilities
+ * 2. node has an unfulfilled requirement that matches the component's capabilities
+ * 3. vl is passed in which has the capability to fulfill requirement from component and requirement on node.
+ */
+ public findMatchingNodesToComponentInstance(componentInstance: ComponentInstance, nodeDataArray: CompositionCiNodeBase[], links: CompositionCiLinkBase[]): any[] {
+ return _.filter(nodeDataArray, (node: CompositionCiNodeBase) => {
+ const matchedRequirementsCapabilities = this.getMatchedRequirementsCapabilities(node.componentInstance, componentInstance, links);
+ return matchedRequirementsCapabilities && matchedRequirementsCapabilities.length > 0;
+ });
+ }
+
+ public 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 isRequirementFulfilled(fromNodeId: string, requirement: any, links: CompositionCiLinkBase[]): boolean {
+ return _.some(links, {
+ relation: {
+ fromNode: fromNodeId,
+ relationships: [{
+ relation: {
+ requirementOwnerId: requirement.ownerId,
+ requirement: requirement.name,
+ relationship: {
+ type: requirement.relationship
+ }
+
+ }
+ }]
+ }
+ });
+ }
+
+}